From f43e543e844a931df2a7882445c3e01268ed7d3a Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 23 Mar 2023 14:21:31 +0200 Subject: [PATCH 001/103] UI: color picker --- ui-ngx/package.json | 1 + .../color-picker-input.component.html | 18 +++ .../color-picker-input.component.ts | 70 +++++++++++ .../color-picker/color-picker.component.html | 37 ++++++ .../color-picker/color-picker.component.scss | 110 ++++++++++++++++++ .../color-picker/color-picker.component.ts | 106 +++++++++++++++++ .../dialog/color-picker-dialog.component.html | 13 ++- .../dialog/color-picker-dialog.component.ts | 18 ++- ui-ngx/src/app/shared/shared.module.ts | 11 +- ui-ngx/yarn.lock | 7 ++ 10 files changed, 380 insertions(+), 11 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html create mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts create mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker.component.html create mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss create mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts diff --git a/ui-ngx/package.json b/ui-ngx/package.json index a8abb6fbeb..96577444fd 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -31,6 +31,7 @@ "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "~0.6.0", "@geoman-io/leaflet-geoman-free": "^2.13.0", + "@iplab/ngx-color-picker": "^15.0.2", "@juggle/resize-observer": "^3.3.1", "@mat-datetimepicker/core": "~11.0.3", "@material-ui/core": "4.12.3", diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html new file mode 100644 index 0000000000..564f1d64f2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html @@ -0,0 +1,18 @@ +
+
+ + + + + + + + + +
+ + + + +
+ diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts new file mode 100644 index 0000000000..22ce2c0849 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts @@ -0,0 +1,70 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output +} from '@angular/core'; +import { Color } from '@iplab/ngx-color-picker'; +import { ColorType } from '@shared/components/color-picker/color-picker.component'; + +@Component({ + selector: `tb-color-picker-input-component`, + templateUrl: `./color-picker-input.component.html`, + styleUrls: [], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ColorPickerInputComponent { + + @Input() + public hue: Color; + + @Output() + public hueChange = new EventEmitter(false); + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + public labelVisible: boolean; + + @Input() + public set label(value) { + this.labelVisible = true; + } + + @Input() + public colorFormat: string; + + public isAlphaVisible = true; + + @Input() + public set alpha(isVisible: boolean) { + this.isAlphaVisible = isVisible; + } + + public get value() { + return this.color ? this.color.getRgba() : null; + } + + constructor() { + } + + public onInputChange(newValue, color: 'R' | 'G' | 'B' | 'A') { + const value = this.value; + const red = color === 'R' ? newValue : value.red; + const green = color === 'G' ? newValue : value.green; + const blue = color === 'B' ? newValue : value.blue; + const alpha = color === 'A' ? newValue : value.alpha; + + const newColor = new Color().setRgba(red, green, blue, alpha); + const hue = new Color().setHsva(newColor.getHsva().hue); + + this.hueChange.emit(hue); + this.colorChange.emit(newColor); + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html new file mode 100644 index 0000000000..dba4605736 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -0,0 +1,37 @@ + + +
+
+
+ +
+
+ + +
+
+ + + + + {{type}} + + + + + + + + + + + + + + + + + +
+ + diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss new file mode 100644 index 0000000000..39a0c7444c --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -0,0 +1,110 @@ +:host { + display: block; + width: 100%; + max-width: 320px; + min-width: 100%; + border-radius: 2px; + background: #fff; + + ::ng-deep { + .saturation-component { + height: 200px; + border-radius: 8px; + div.pointer { + border-width: 2px; + width: 16px; + height: 16px; + } + } + + indicator-component { + svg { + vertical-align: -70% !important; + } + } + + .hue-alpha-range { + alpha-component, hue-component { + height: 18px; + border: 1px solid rgba(0, 0, 0, 0.1); + .pointer { + height: 18px; + width: 18px; + background: none; + border: 2px solid #fff; + } + .gradient-color { + border-radius: 24px; + } + } + } + } +} + + + +.controls { + padding: 15px 0; +} + +.controls-row { + display: table; + width: 100%; +} + +.column { + display: table-cell; + vertical-align: middle; +} + +.hue-alpha .column:first-child { + width: 42px; + padding: 0 10px 0 0; +} + +indicator-component { + height: 48px; + width: 48px; + border-radius: 8px; + background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==') repeat; +} + +.hue-alpha-range { + hue-component, alpha-component { + border-radius: 24px; + } +} + +hue-component { + margin-bottom: 16px; +} + +color-presets-component { + border-top: 1px solid #d0d0d0; + padding: 12px; + + ::ng-deep .presets-row { + padding: 12px 0 0; + } +} + +.type-btn { + display: inline-block; + height: 20px; + width: 20px; + background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAYAAAAffCjxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACewAAAnsB01CO3AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIASURBVEiJ7ZY9axRRFIafsxMStrLQJpAgpBFhi+C9w1YSo00I6RZ/g9vZpBf/QOr4GyRgkSKNSrAadsZqQGwCkuAWyRZJsySwvhZ7N/vhzrgbLH3Ld8597jlzz50zJokyxXH8DqDVar0qi6v8BbItqSGpEcfxdlmsFWXkvX8AfAVWg3UKPEnT9GKujMzsAFgZsVaCN1VTQd77XUnrgE1kv+6935268WRpzrnHZvYRWC7YvC3pRZZl3wozqtVqiyH9IgjAspkd1Gq1xUJQtVrdB9ZKIAOthdg/Qc65LUk7wNIMoCVJO865rYFhkqjX6/d7vV4GPJwBMqofURS5JEk6FYBer/eeYb/Mo9WwFnPOvQbeAvfuAAK4BN4sAJtAG/gJIElmNuiJyba3EGNmZiPeZuEVmVell/Y/6N+CzDn3AXhEOOo7Hv/3BeAz8IzQkMPnJbuPx1wC+yYJ7/0nYIP5S/0FHKdp+rwCEEXRS/rf5Hl1Gtb2M0iSpCOpCZzPATmX1EySpHMLAsiy7MjMDoHrGSDXZnaYZdnRwBh7J91utwmczAA6CbG3GgPleX4jqUH/a1CktqRGnuc3hSCAMB32gKspkCtgb3KCQMmkjeP4WNJThrNNZval1WptTIsv7JtQ4tmIdRa8qSoEpWl6YWZNoAN0zKxZNPehpLSBZv2t+Q0CJ9lLnARQLAAAAABJRU5ErkJggg==') no-repeat center; + background-size: 6px 12px; + + &:hover { + background-color: #eee; + } +} + +.type-column { + width: 25px; + text-align: right; +} + +.presentation { + padding: 12px 0 0; +} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts new file mode 100644 index 0000000000..e11d11f880 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts @@ -0,0 +1,106 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, SimpleChanges +} from '@angular/core'; +import { Color, ColorPickerControl } from '@iplab/ngx-color-picker'; +import { Subscription } from 'rxjs'; +export enum ColorType { + hex = 'hex', + hexa = 'hexa', + rgba = 'rgba', + rgb = 'rgb', + hsla = 'hsla', + hsl = 'hsl', + cmyk = 'cmyk' +} + +@Component({ + selector: `tb-color-picker`, + templateUrl: `./color-picker.component.html`, + styleUrls: [`./color-picker.component.scss`], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { + + public selectedPresentation = 0; + public presentations = [ColorType.hex, ColorType.hexa, ColorType.rgb, ColorType.rgba, ColorType.hsl, ColorType.hsla]; + + @Input() + public color: string; + + @Input() + public control: ColorPickerControl; + + @Output() + public colorChange: EventEmitter = new EventEmitter(false); + + private subscriptions: Array = []; + + constructor(private readonly cdr: ChangeDetectorRef) { + } + + public ngOnInit(): void { + if (!this.control) { + this.control = new ColorPickerControl(); + } + + if (this.color) { + this.control.setValueFrom(this.color); + } + + this.subscriptions.push( + this.control.valueChanges.subscribe((value) => { + this.cdr.markForCheck(); + this.colorChange.emit(this.getValueByType(value, this.control.initType)); + }) + ); + } + + changeColorFormat(event: Event) { + this.colorChange.emit(this.getValueByType(this.control.value, this.control.initType)); + } + + public ngOnDestroy(): void { + this.cdr.detach(); + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + this.subscriptions.length = 0; + } + + public ngOnChanges(changes: SimpleChanges): void { + if (this.color && this.control && this.getValueByType(this.control.value, this.control.initType) !== this.color) { + this.control.setValueFrom(this.color); + } + } + + public changePresentation(): void { + this.selectedPresentation = + this.selectedPresentation === this.presentations.length - 1 ? 0 : this.selectedPresentation + 1; + } + + getValueByType(color: Color, type: ColorType): string { + switch (type) { + case ColorType.hex: + return color.toHexString(); + case ColorType.hexa: + return color.toHexString(true); + case ColorType.rgb: + return color.toRgbString(); + case ColorType.rgba: + return color.toRgbaString(); + case ColorType.hsl: + return color.toHslString(); + case ColorType.hsla: + return color.toHslaString(); + default: + return color.toRgbaString(); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index a7d4835547..4ea0a5f8c5 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -17,12 +17,13 @@ -->
- - + + + + + + +
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index e986ddc331..784da8da25 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -19,9 +19,19 @@ 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 { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { + FormGroupDirective, + NgForm, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; +import { ColorPickerControl } from '@iplab/ngx-color-picker'; +import { ColorType } from '@shared/components/color-picker/color-picker.component'; +import * as tinycolor_ from 'tinycolor2'; export interface ColorPickerDialogData { color: string; @@ -40,12 +50,16 @@ export class ColorPickerDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder) { + public fb: UntypedFormBuilder,) { super(store, router, dialogRef); } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 950532adab..10dbbef4aa 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -62,7 +62,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { ShareModule as ShareButtonsModule } from 'ngx-sharebuttons'; import { HotkeyModule } from 'angular2-hotkeys'; -import { ColorPickerModule } from 'ngx-color-picker'; +import { ColorPickerModule } from '@iplab/ngx-color-picker'; import { NgxHmCarouselModule } from 'ngx-hm-carousel'; import { UserMenuComponent } from '@shared/components/user-menu.component'; import { NospacePipe } from '@shared/pipe/nospace.pipe'; @@ -169,6 +169,8 @@ import { CustomDateAdapter } from '@shared/adapter/custom-datatime-adapter'; import { CustomPaginatorIntl } from '@shared/services/custom-paginator-intl'; import { TbScriptLangComponent } from '@shared/components/script-lang.component'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; +import { ColorPickerInputComponent } from '@shared/components/color-picker/color-picker-input.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -309,7 +311,9 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) BranchAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, - DateAgoPipe + DateAgoPipe, + ColorPickerComponent, + ColorPickerInputComponent ], imports: [ CommonModule, @@ -514,7 +518,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) BranchAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, - DateAgoPipe + DateAgoPipe, + ColorPickerComponent ] }) export class SharedModule { } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index b939f898f1..e4c90c98f3 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1565,6 +1565,13 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@iplab/ngx-color-picker@^15.0.2": + version "15.0.2" + resolved "https://registry.yarnpkg.com/@iplab/ngx-color-picker/-/ngx-color-picker-15.0.2.tgz#856bf2571378e792e5e42b566ac1aa79a9262763" + integrity sha512-wum0Hg4Ky/6mhvzolEpFpjGckOjN8L2nkXvy3mWUVclFHP9MZqqgpJaxghtax7/tMdEWQnwQI3PYsNyKfF15ug== + dependencies: + tslib "^2.3.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" From 1fee9a327302a0f20498267ed62715a22eb76908 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 29 Mar 2023 11:40:32 +0300 Subject: [PATCH 002/103] Revert "Add quarter and half year intervals to time window." --- .../src/app/shared/models/time/time.models.ts | 68 ------------------- .../assets/locale/locale.constant-en_US.json | 6 -- 2 files changed, 74 deletions(-) diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index ae093c71d2..6c04aef946 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -140,8 +140,6 @@ export enum QuickTimeInterval { PREVIOUS_WEEK = 'PREVIOUS_WEEK', PREVIOUS_WEEK_ISO = 'PREVIOUS_WEEK_ISO', PREVIOUS_MONTH = 'PREVIOUS_MONTH', - PREVIOUS_QUARTER = 'PREVIOUS_QUARTER', - PREVIOUS_HALF_YEAR = 'PREVIOUS_HALF_YEAR', PREVIOUS_YEAR = 'PREVIOUS_YEAR', CURRENT_HOUR = 'CURRENT_HOUR', CURRENT_DAY = 'CURRENT_DAY', @@ -152,10 +150,6 @@ export enum QuickTimeInterval { CURRENT_WEEK_ISO_SO_FAR = 'CURRENT_WEEK_ISO_SO_FAR', CURRENT_MONTH = 'CURRENT_MONTH', CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR', - CURRENT_QUARTER = 'CURRENT_QUARTER', - CURRENT_QUARTER_SO_FAR = 'CURRENT_QUARTER_SO_FAR', - CURRENT_HALF_YEAR = 'CURRENT_HALF_YEAR', - CURRENT_HALF_YEAR_SO_FAR = 'CURRENT_HALF_YEAR_SO_FAR', CURRENT_YEAR = 'CURRENT_YEAR', CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR' } @@ -167,8 +161,6 @@ export const QuickTimeIntervalTranslationMap = new Map Date: Fri, 31 Mar 2023 11:40:04 +0300 Subject: [PATCH 003/103] UI: color-picker --- .../color-picker-input.component.html | 25 ++++++++++++++++--- .../color-picker-input.component.ts | 16 ++++++++++++ .../color-picker/color-picker.component.html | 19 ++++++++++++++ .../color-picker/color-picker.component.scss | 15 +++++++++++ .../color-picker/color-picker.component.ts | 16 ++++++++++++ .../dialog/color-picker-dialog.component.html | 2 +- 6 files changed, 88 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html index 564f1d64f2..c135369793 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html @@ -1,18 +1,35 @@ +
- + - + - +
- +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts index 22ce2c0849..445b65a969 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + import { ChangeDetectionStrategy, ChangeDetectorRef, diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index dba4605736..4751d498f2 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -1,3 +1,20 @@ +
@@ -11,6 +28,7 @@
+
@@ -32,6 +50,7 @@ +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index 39a0c7444c..24fd427576 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ :host { display: block; width: 100%; diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts index e11d11f880..f2140e02a5 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + import { ChangeDetectionStrategy, ChangeDetectorRef, diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index 4ea0a5f8c5..8de970de65 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -16,7 +16,7 @@ --> -
+
From 5476b4a49d0955174c4de16f6c52c8b6b8c143f6 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 31 Mar 2023 12:26:20 +0300 Subject: [PATCH 004/103] Update PR template delete add version in templates --- pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pull_request_template.md b/pull_request_template.md index 21362429dc..d0bf8993f1 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -5,7 +5,7 @@ Put your PR description here instead of this sentence. ## General checklist - [ ] You have reviewed the guidelines [document](https://docs.google.com/document/d/1wqcOafLx5hth8SAg4dqV_LV3un3m5WYR8RdTJ4MbbUM/edit?usp=sharing). -- [ ] PR name contains fix version. For example, "[3.3.4] Hotfix of some UI component" or "[3.4] New Super Feature". +- [ ] [Labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels#about-labels) that classify your pull request have been added. - [ ] The [milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) is specified and corresponds to fix version. - [ ] Description references specific [issue](https://github.com/thingsboard/thingsboard/issues). - [ ] Description contains human-readable scope of changes. From 0987c65681509f536a477623beb30fceb6d1301f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 12 Apr 2023 13:00:51 +0300 Subject: [PATCH 005/103] UI: color-picker --- .../color-picker-input.component.html | 35 -------- .../color-picker-input.component.ts | 86 ------------------- .../color-picker/color-picker.component.html | 35 +++----- .../color-picker/color-picker.component.ts | 27 ++++-- .../dialog/color-picker-dialog.component.html | 8 +- .../dialog/color-picker-dialog.component.ts | 4 - ui-ngx/src/app/shared/shared.module.ts | 4 +- 7 files changed, 32 insertions(+), 167 deletions(-) delete mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html delete mode 100644 ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html deleted file mode 100644 index c135369793..0000000000 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.html +++ /dev/null @@ -1,35 +0,0 @@ - -
-
- - - - - - - - - -
- - - - -
- diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts deleted file mode 100644 index 445b65a969..0000000000 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-input.component.ts +++ /dev/null @@ -1,86 +0,0 @@ -/// -/// Copyright © 2016-2023 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnInit, - Output -} from '@angular/core'; -import { Color } from '@iplab/ngx-color-picker'; -import { ColorType } from '@shared/components/color-picker/color-picker.component'; - -@Component({ - selector: `tb-color-picker-input-component`, - templateUrl: `./color-picker-input.component.html`, - styleUrls: [], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ColorPickerInputComponent { - - @Input() - public hue: Color; - - @Output() - public hueChange = new EventEmitter(false); - - @Input() - public color: Color; - - @Output() - public colorChange = new EventEmitter(false); - - public labelVisible: boolean; - - @Input() - public set label(value) { - this.labelVisible = true; - } - - @Input() - public colorFormat: string; - - public isAlphaVisible = true; - - @Input() - public set alpha(isVisible: boolean) { - this.isAlphaVisible = isVisible; - } - - public get value() { - return this.color ? this.color.getRgba() : null; - } - - constructor() { - } - - public onInputChange(newValue, color: 'R' | 'G' | 'B' | 'A') { - const value = this.value; - const red = color === 'R' ? newValue : value.red; - const green = color === 'G' ? newValue : value.green; - const blue = color === 'B' ? newValue : value.blue; - const alpha = color === 'A' ? newValue : value.alpha; - - const newColor = new Color().setRgba(red, green, blue, alpha); - const hue = new Color().setHsva(newColor.getHsva().hue); - - this.hueChange.emit(hue); - this.colorChange.emit(newColor); - } -} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index 4751d498f2..1005fa4a58 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -29,28 +29,17 @@
- - - - {{type}} - - - - - - - - - - - - - - - - - +
+
+ + + + + +
+
+ +
+
- - diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts index f2140e02a5..ab8fb847dd 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts @@ -23,10 +23,12 @@ import { OnChanges, OnDestroy, OnInit, - Output, SimpleChanges + Output, + SimpleChanges } from '@angular/core'; import { Color, ColorPickerControl } from '@iplab/ngx-color-picker'; import { Subscription } from 'rxjs'; + export enum ColorType { hex = 'hex', hexa = 'hexa', @@ -46,7 +48,7 @@ export enum ColorType { export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { public selectedPresentation = 0; - public presentations = [ColorType.hex, ColorType.hexa, ColorType.rgb, ColorType.rgba, ColorType.hsl, ColorType.hsla]; + public presentations = [ColorType.hex, ColorType.rgb, ColorType.rgba, ColorType.hsla, ColorType.hsl]; @Input() public color: string; @@ -67,6 +69,12 @@ export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { this.control = new ColorPickerControl(); } + if (this.control.initType === ColorType.hexa) { + this.control.initType = ColorType.hex; + } + + this.selectedPresentation = this.presentations.indexOf(this.control.initType); + if (this.color) { this.control.setValueFrom(this.color); } @@ -74,13 +82,13 @@ export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { this.subscriptions.push( this.control.valueChanges.subscribe((value) => { this.cdr.markForCheck(); - this.colorChange.emit(this.getValueByType(value, this.control.initType)); + this.colorChange.emit(this.getValueByType(value, this.presentations[this.selectedPresentation])); }) ); } changeColorFormat(event: Event) { - this.colorChange.emit(this.getValueByType(this.control.value, this.control.initType)); + this.colorChange.emit(this.getValueByType(this.control.value, this.presentations[this.selectedPresentation])); } public ngOnDestroy(): void { @@ -90,7 +98,8 @@ export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { } public ngOnChanges(changes: SimpleChanges): void { - if (this.color && this.control && this.getValueByType(this.control.value, this.control.initType) !== this.color) { + if (this.color && this.control && + this.getValueByType(this.control.value, this.presentations[this.selectedPresentation]) !== this.color) { this.control.setValueFrom(this.color); } } @@ -98,14 +107,14 @@ export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { public changePresentation(): void { this.selectedPresentation = this.selectedPresentation === this.presentations.length - 1 ? 0 : this.selectedPresentation + 1; + this.colorChange.emit(this.getValueByType(this.control.value, this.presentations[this.selectedPresentation])); + this.cdr.markForCheck(); } getValueByType(color: Color, type: ColorType): string { switch (type) { - case ColorType.hex: - return color.toHexString(); - case ColorType.hexa: - return color.toHexString(true); + case ColorType.hex || ColorType.hexa: + return color.toHexString(this.control.value.getRgba().getAlpha() !== 1); case ColorType.rgb: return color.toRgbString(); case ColorType.rgba: diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index 8de970de65..aedeff35b2 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -15,14 +15,8 @@ limitations under the License. --> - +
- - - - - -
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 784da8da25..8a9d5cbdf8 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -30,8 +30,6 @@ import { import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { ColorPickerControl } from '@iplab/ngx-color-picker'; -import { ColorType } from '@shared/components/color-picker/color-picker.component'; -import * as tinycolor_ from 'tinycolor2'; export interface ColorPickerDialogData { color: string; @@ -50,8 +48,6 @@ export class ColorPickerDialogComponent extends DialogComponent, diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 10dbbef4aa..e05275a7af 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -170,7 +170,6 @@ import { CustomPaginatorIntl } from '@shared/services/custom-paginator-intl'; import { TbScriptLangComponent } from '@shared/components/script-lang.component'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; -import { ColorPickerInputComponent } from '@shared/components/color-picker/color-picker-input.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -312,8 +311,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) PhoneInputComponent, TbScriptLangComponent, DateAgoPipe, - ColorPickerComponent, - ColorPickerInputComponent + ColorPickerComponent ], imports: [ CommonModule, From dae9a422a15e02999ca6447d3c3aa0375a3f37d8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 12 Apr 2023 16:44:24 +0300 Subject: [PATCH 006/103] Fix entities ids replaced with tenant id in dashboard config when importing --- .../impl/BaseEntityImportService.java | 46 +++++++--------- .../impl/DashboardImportService.java | 4 +- .../DefaultEntitiesVersionControlService.java | 6 +- .../sync/ie/ExportImportServiceSqlTest.java | 55 ++++++++++++------- monitoring/src/main/conf/logback.xml | 7 ++- 5 files changed, 66 insertions(+), 52 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index f72ac8a11f..610f1ee0b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; @@ -87,7 +86,6 @@ public abstract class BaseEntityImportService importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException { EntityImportResult importResult = new EntityImportResult<>(); @@ -336,27 +334,37 @@ public abstract class BaseEntityImportService externalIdOpt = buildEntityId(entityType, externalUuid); - if (!externalIdOpt.isPresent()) { + Optional externalId = buildEntityId(entityType, externalUuid); + if (externalId.isEmpty()) { continue; } - EntityId internalId = ctx.getInternalId(externalIdOpt.get()); + EntityId internalId = ctx.getInternalId(externalId.get()); if (internalId != null) { return Optional.of(internalId); } } if (fetchAllUUIDs) { - for (EntityType entityType : hints) { - Optional internalId = lookupInDb(externalUuid, entityType); - if (internalId.isPresent()) return internalId; - } + Set processLast = Set.of(EntityType.TENANT); + List entityTypes = new ArrayList<>(hints); for (EntityType entityType : EntityType.values()) { - if (hints.contains(entityType)) { + if (!hints.contains(entityType) && !processLast.contains(entityType)) { + entityTypes.add(entityType); + } + } + entityTypes.addAll(processLast); + + for (EntityType entityType : entityTypes) { + Optional externalId = buildEntityId(entityType, externalUuid); + if (externalId.isEmpty() || ctx.isNotFound(externalId.get())) { continue; } - Optional internalId = lookupInDb(externalUuid, entityType); - if (internalId.isPresent()) return internalId; + EntityId internalId = getInternalId(externalId.get(), false); + if (internalId != null) { + return Optional.of(internalId); + } else { + ctx.registerNotFound(externalId.get()); + } } } @@ -364,20 +372,6 @@ public abstract class BaseEntityImportService lookupInDb(UUID externalUuid, EntityType entityType) { - Optional externalIdOpt = buildEntityId(entityType, externalUuid); - if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) { - return Optional.empty(); - } - EntityId internalId = getInternalId(externalIdOpt.get(), false); - if (internalId != null) { - return Optional.of(internalId); - } else { - ctx.registerNotFound(externalIdOpt.get()); - } - return Optional.empty(); - } - private Optional buildEntityId(EntityType entityType, UUID externalUuid) { try { return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid)); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index 611b536808..250026c71c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -65,10 +65,10 @@ public class DashboardImportService extends BaseEntityImportService exportData, IdProvider idProvider) { for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) { - replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS); + replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), HINTS); } for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { - replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS); + replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), HINTS); } return dashboard; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 8ffd4796fe..0e08ee9c48 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; @@ -69,6 +68,7 @@ import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersion import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; +import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; @@ -259,7 +259,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId); } - @SuppressWarnings({"UnstableApiUsage", "rawtypes"}) + @SuppressWarnings({"rawtypes"}) @Override public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception { EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId()); @@ -402,7 +402,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>()) .add(importResult.getSavedEntity().getId()); } - log.debug("Imported {} pack for tenant {}", entityType, ctx.getTenantId()); + log.debug("Imported {} pack ({}) for tenant {}", entityType, entityDataList.size(), ctx.getTenantId()); offset += limit; } while (entityDataList.size() == limit); } diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 26e8b3e92a..e493916b8a 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -68,7 +68,6 @@ import org.thingsboard.server.service.ota.OtaPackageStateService; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -254,21 +253,25 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); + DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing"); + String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; + String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa"; String entityAliases = "{\n" + - "\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + - "\t\t\"alias\": \"assets\",\n" + - "\t\t\"filter\": {\n" + - "\t\t\t\"entityList\": [\n" + - "\t\t\t\t\"" + asset1.getId().toString() + "\",\n" + - "\t\t\t\t\"" + asset2.getId().toString() + "\"\n" + - "\t\t\t],\n" + - "\t\t\t\"entityType\": \"ASSET\",\n" + - "\t\t\t\"resolveMultiple\": true,\n" + - "\t\t\t\"type\": \"entityList\"\n" + - "\t\t},\n" + - "\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + - "\t}\n" + + "\"" + aliasId + "\": {\n" + + "\"alias\": \"assets\",\n" + + "\"filter\": {\n" + + "\"entityList\": [\n" + + "\"" + asset1.getId().toString() + "\",\n" + + "\"" + asset2.getId().toString() + "\",\n" + + "\"" + tenantId1.getId().toString() + "\",\n" + + "\"" + existingDeviceProfile.getId().toString() + "\",\n" + + "\"" + unknownUuid + "\"\n" + + "],\n" + + "\"resolveMultiple\": true\n" + + "},\n" + + "\"id\": \"" + aliasId + "\"\n" + + "}\n" + "}"; ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); @@ -287,11 +290,23 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); - Set entityAliasEntitiesIds = Streams.stream(importedDashboard.getConfiguration() - .get("entityAliases").elements().next().get("filter").get("entityList").elements()) - .map(JsonNode::asText).collect(Collectors.toSet()); - assertThat(entityAliasEntitiesIds).doesNotContain(asset1.getId().toString(), asset2.getId().toString()); - assertThat(entityAliasEntitiesIds).contains(importedAsset1.getId().toString(), importedAsset2.getId().toString()); + Map.Entry entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next(); + assertThat(entityAlias.getKey()).isEqualTo(aliasId); + assertThat(entityAlias.getValue().get("id").asText()).isEqualTo(aliasId); + + List aliasEntitiesIds = Streams.stream(entityAlias.getValue().get("filter").get("entityList").elements()) + .map(JsonNode::asText).collect(Collectors.toList()); + assertThat(aliasEntitiesIds).size().isEqualTo(5); + assertThat(aliasEntitiesIds).element(0).as("external asset 1 was replaced with imported one") + .isEqualTo(importedAsset1.getId().toString()); + assertThat(aliasEntitiesIds).element(1).as("external asset 2 was replaced with imported one") + .isEqualTo(importedAsset2.getId().toString()); + assertThat(aliasEntitiesIds).element(2).as("external tenant id was replaced with new tenant id") + .isEqualTo(tenantId2.toString()); + assertThat(aliasEntitiesIds).element(3).as("existing device profile id was left as is") + .isEqualTo(existingDeviceProfile.getId().toString()); + assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id") + .isEqualTo(tenantId2.toString()); } @@ -469,7 +484,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml index 58e49c7639..3c1e1382b7 100644 --- a/monitoring/src/main/conf/logback.xml +++ b/monitoring/src/main/conf/logback.xml @@ -33,14 +33,19 @@ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + - + From f835f74f1c50687ef58f92d9e89cb00edcb3be56 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Apr 2023 16:12:31 +0300 Subject: [PATCH 007/103] UI: Fixed template notification view --- ui-ngx/package.json | 1 - .../components/color-picker/color-picker.component.scss | 4 ++-- ui-ngx/yarn.lock | 7 ------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 557c288049..2c29990466 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -74,7 +74,6 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.42", "ngx-clipboard": "^15.1.0", - "ngx-color-picker": "^14.0.0", "ngx-daterangepicker-material": "^6.0.4", "ngx-drag-drop": "^15.0.1", "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/2.0.0", diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index 24fd427576..38b55346aa 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -81,7 +81,7 @@ indicator-component { height: 48px; width: 48px; border-radius: 8px; - background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==') repeat; + background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMUAFg9Gm0AAAAPSURBVAjXY2D4jxXhEgYAfr8P8QhChVEAAAAASUVORK5CYII=') repeat; } .hue-alpha-range { @@ -107,7 +107,7 @@ color-presets-component { display: inline-block; height: 20px; width: 20px; - background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAYAAAAffCjxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACewAAAnsB01CO3AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIASURBVEiJ7ZY9axRRFIafsxMStrLQJpAgpBFhi+C9w1YSo00I6RZ/g9vZpBf/QOr4GyRgkSKNSrAadsZqQGwCkuAWyRZJsySwvhZ7N/vhzrgbLH3Ld8597jlzz50zJokyxXH8DqDVar0qi6v8BbItqSGpEcfxdlmsFWXkvX8AfAVWg3UKPEnT9GKujMzsAFgZsVaCN1VTQd77XUnrgE1kv+6935268WRpzrnHZvYRWC7YvC3pRZZl3wozqtVqiyH9IgjAspkd1Gq1xUJQtVrdB9ZKIAOthdg/Qc65LUk7wNIMoCVJO865rYFhkqjX6/d7vV4GPJwBMqofURS5JEk6FYBer/eeYb/Mo9WwFnPOvQbeAvfuAAK4BN4sAJtAG/gJIElmNuiJyba3EGNmZiPeZuEVmVell/Y/6N+CzDn3AXhEOOo7Hv/3BeAz8IzQkMPnJbuPx1wC+yYJ7/0nYIP5S/0FHKdp+rwCEEXRS/rf5Hl1Gtb2M0iSpCOpCZzPATmX1EySpHMLAsiy7MjMDoHrGSDXZnaYZdnRwBh7J91utwmczAA6CbG3GgPleX4jqUH/a1CktqRGnuc3hSCAMB32gKspkCtgb3KCQMmkjeP4WNJThrNNZval1WptTIsv7JtQ4tmIdRa8qSoEpWl6YWZNoAN0zKxZNPehpLSBZv2t+Q0CJ9lLnARQLAAAAABJRU5ErkJggg==') no-repeat center; + background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAMAAAAootjDAAAAM1BMVEUAAAAzMzMzMzMzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzMyMjIrKyszMzPF8UZlAAAAEHRSTlMA1fHr4ZxxSRP45sG+sCkGH2+Z6QAAAHJJREFUKM+9kkkSgCAQA0FEVLb5/2tViqgQvNrHviSzKGCt6nDGuNass8i8NsrLiX+bZbrUtDwm7VLYE0zWUtEZ+RvUZpEvN8YhH9QmQRoC8kFpEnVHVP/DJUZVeSAem5fDKxwtms/BR+PT8gN8vwk/0wE1gQzNVYryIwAAAABJRU5ErkJggg==') no-repeat center; background-size: 6px 12px; &:hover { diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 7625732209..7764dcbe5d 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -8137,13 +8137,6 @@ ngx-clipboard@^15.1.0: ngx-window-token ">=6.0.0" tslib "^2.0.0" -ngx-color-picker@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/ngx-color-picker/-/ngx-color-picker-14.0.0.tgz#4587f517ac5683a705d4e55cd0939afa91faa853" - integrity sha512-w28zx2DyVpIJeNsTB3T2LUI4Ed/Ujf5Uhxuh0dllputfpxXwZG9ocSJM/0L67+fxA3UnfvvXVZNUX1Ny5nZIIw== - dependencies: - tslib "^2.3.0" - ngx-daterangepicker-material@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-6.0.4.tgz#4e2432698ded28021a5fc0b0025dd1e0a3a13412" From b09bbff15d293a1e07e08f53e95f9d89180d2d1c Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 14 Apr 2023 16:13:10 +0200 Subject: [PATCH 008/103] fixed RestTemplate xml content type issue --- .../system/RestTemplateConvertersTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java new file mode 100644 index 0000000000..6a8491304d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.system; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.springframework.util.ClassUtils; +import org.springframework.web.client.RestTemplate; + + +@Slf4j +public class RestTemplateConvertersTest { + + @Test + public void testJacksonXmlConverter() { + ClassLoader classLoader = RestTemplate.class.getClassLoader(); + boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); + Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!"); + //If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate + } + +} From 08efbd1c85bafb217bf4e825930b0a5077c1ed43 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 14 Apr 2023 18:06:59 +0200 Subject: [PATCH 009/103] revert tskv value noxss validation --- application/src/main/resources/thingsboard.yml | 2 -- .../server/controller/BaseTelemetryControllerTest.java | 6 ++---- .../server/common/data/kv/JsonDataEntry.java | 3 --- .../server/common/data/kv/StringDataEntry.java | 5 +---- .../server/dao/attributes/AttributeUtils.java | 8 ++++---- .../server/dao/attributes/BaseAttributesService.java | 7 ++----- .../server/dao/attributes/CachedAttributesService.java | 7 ++----- .../server/dao/timeseries/BaseTimeseriesService.java | 9 +++------ .../java/org/thingsboard/server/dao/util/KvUtils.java | 10 ++++------ .../server/dao/service/ConstraintValidatorTest.java | 7 ------- 10 files changed, 18 insertions(+), 46 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 1c1036dc14..14bf47301b 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -259,13 +259,11 @@ sql: batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution - noxss_validation_enabled: "${SQL_ATTRIBUTES_NOXSS_VALIDATION_ENABLED:true}" ts: batch_size: "${SQL_TS_BATCH_SIZE:10000}" batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution - noxss_validation_enabled: "${SQL_TS_NOXSS_VALIDATION_ENABLED:true}" ts_latest: batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}" batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java index af0d486869..50ad731a79 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import org.junit.Test; +import org.stringtemplate.v4.ST; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -32,10 +33,7 @@ public abstract class BaseTelemetryControllerTest extends AbstractControllerTest String correctRequestBody = "{\"data\": \"value\"}"; doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk()); - String invalidRequestBody = "{\"data\": \"alert(document)\\\">\"}"; - doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); - doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); - invalidRequestBody = "{\"alert(document)\\\">\": \"data\"}"; + String invalidRequestBody = "{\"alert(document)\\\">\": \"data\"}"; doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java index 4260c512f8..0736b50b84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java @@ -15,14 +15,11 @@ */ package org.thingsboard.server.common.data.kv; -import org.thingsboard.server.common.data.validation.NoXss; - import java.util.Objects; import java.util.Optional; public class JsonDataEntry extends BasicKvEntry { - @NoXss private final String value; public JsonDataEntry(String key, String value) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java index 9251cc80b3..18a54327a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.common.data.kv; -import org.thingsboard.server.common.data.validation.NoXss; - import java.util.Objects; import java.util.Optional; @@ -24,7 +22,6 @@ public class StringDataEntry extends BasicKvEntry { private static final long serialVersionUID = 1L; - @NoXss private final String value; public StringDataEntry(String key, String value) { @@ -68,7 +65,7 @@ public class StringDataEntry extends BasicKvEntry { public String toString() { return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString(); } - + @Override public String getValueAsString() { return value; diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java index 980057a231..d1abeda5b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java @@ -30,12 +30,12 @@ public class AttributeUtils { Validator.validateString(scope, "Incorrect scope " + scope); } - public static void validate(List kvEntries, boolean validateNoxss) { - kvEntries.forEach(kv -> validate(kv, validateNoxss)); + public static void validate(List kvEntries) { + kvEntries.forEach(AttributeUtils::validate); } - public static void validate(AttributeKvEntry kvEntry, boolean validateNoxss) { - KvUtils.validate(kvEntry, validateNoxss); + public static void validate(AttributeKvEntry kvEntry) { + KvUtils.validate(kvEntry); if (kvEntry.getDataType() == null) { throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null"); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 08cb8beeae..abd55b6b12 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -46,9 +46,6 @@ import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; public class BaseAttributesService implements AttributesService { private final AttributesDao attributesDao; - @Value("${sql.attributes.noxss_validation_enabled:true}") - private boolean noxssValidationEnabled; - public BaseAttributesService(AttributesDao attributesDao) { this.attributesDao = attributesDao; } @@ -86,14 +83,14 @@ public class BaseAttributesService implements AttributesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { validate(entityId, scope); - AttributeUtils.validate(attribute, noxssValidationEnabled); + AttributeUtils.validate(attribute); return attributesDao.save(tenantId, entityId, scope, attribute); } @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - AttributeUtils.validate(attributes, noxssValidationEnabled); + AttributeUtils.validate(attributes); List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index ef81ed695b..b95ce39d9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -70,9 +70,6 @@ public class CachedAttributesService implements AttributesService { @Value("${cache.type:caffeine}") private String cacheType; - @Value("${sql.attributes.noxss_validation_enabled:true}") - private boolean noxssValidationEnabled; - public CachedAttributesService(AttributesDao attributesDao, StatsFactory statsFactory, CacheExecutorService cacheExecutorService, @@ -215,7 +212,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { validate(entityId, scope); - AttributeUtils.validate(attribute, noxssValidationEnabled); + AttributeUtils.validate(attribute); ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor); } @@ -223,7 +220,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - AttributeUtils.validate(attributes, noxssValidationEnabled); + AttributeUtils.validate(attributes); List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 1f5f21acc8..748c57b5dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -81,9 +81,6 @@ public class BaseTimeseriesService implements TimeseriesService { @Value("${database.ts_max_intervals}") private long maxTsIntervals; - @Value("${sql.ts.noxss_validation_enabled:true}") - private boolean noxssValidationEnabled; - @Autowired private TimeseriesDao timeseriesDao; @@ -159,7 +156,7 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { - KvUtils.validate(tsKvEntry, noxssValidationEnabled); + KvUtils.validate(tsKvEntry); validate(entityId); List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L); @@ -177,7 +174,7 @@ public class BaseTimeseriesService implements TimeseriesService { } private ListenableFuture doSave(TenantId tenantId, EntityId entityId, List tsKvEntries, long ttl, boolean saveLatest) { - KvUtils.validate(tsKvEntries, noxssValidationEnabled); + KvUtils.validate(tsKvEntries); int inserts = saveLatest ? INSERTS_PER_ENTRY : INSERTS_PER_ENTRY_WITHOUT_LATEST; List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * inserts); for (TsKvEntry tsKvEntry : tsKvEntries) { @@ -192,7 +189,7 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { - KvUtils.validate(tsKvEntries, noxssValidationEnabled); + KvUtils.validate(tsKvEntries); List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size()); for (TsKvEntry tsKvEntry : tsKvEntries) { futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java index 133fb4c85e..8e80bd992d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java @@ -22,16 +22,14 @@ import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.List; public class KvUtils { - public static void validate(List tsKvEntries, boolean validateNoxss) { - tsKvEntries.forEach(kvEntry -> validate(kvEntry, validateNoxss)); + public static void validate(List tsKvEntries) { + tsKvEntries.forEach(KvUtils::validate); } - public static void validate(KvEntry tsKvEntry, boolean validateNoxss) { + public static void validate(KvEntry tsKvEntry) { if (tsKvEntry == null) { throw new IncorrectParameterException("Key value entry can't be null"); } - if (validateNoxss) { - ConstraintValidator.validateFields(tsKvEntry); - } + ConstraintValidator.validateFields(tsKvEntry); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java index fa3d39df30..24a32b6a61 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service; import org.junit.Assert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.exception.DataValidationException; @@ -30,15 +29,9 @@ class ConstraintValidatorTest { @Test void validateFields() { StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value"); - StringDataEntry stringDataEntryInvalid1 = new StringDataEntry("", "value"); - StringDataEntry stringDataEntryInvalid2 = new StringDataEntry("key", ""); - - JsonDataEntry jsonDataEntryInvalid = new JsonDataEntry("key", "{\"value\": }"); Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(stringDataEntryInvalid1)); - Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(stringDataEntryInvalid2)); - Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(jsonDataEntryInvalid)); ConstraintValidator.validateFields(stringDataEntryValid); } From bcc5eba8e6e136eb8fe0ecb0eb14a2afecf8e834 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 14 Apr 2023 19:18:35 +0300 Subject: [PATCH 010/103] UI: Tenant admin home page initial implementation --- .../getting-started-widget.component.html | 112 +++ .../getting-started-widget.component.scss | 117 ++- .../getting-started-widget.component.ts | 4 +- .../lib/home-page/home-page-widgets.module.ts | 7 +- .../home-page/toggle-header.component.html | 20 + .../home-page/toggle-header.component.scss | 87 +++ .../lib/home-page/toggle-header.component.ts | 82 +++ .../home-links/home-links-routing.module.ts | 15 +- .../home-links/tenant_admin_home_page.raw | 667 ++++++++++++++++++ .../shared/components/markdown.component.html | 2 +- .../shared/components/markdown.component.ts | 127 ++-- .../assets/locale/locale.constant-en_US.json | 49 ++ 12 files changed, 1216 insertions(+), 73 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html index b2425a0231..ee84a63ad6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -91,6 +91,118 @@ description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} + + + +
+
widgets.getting-started.tenant-admin.step1.title
+ {{ 'device.devices' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step2.title
+
+
+
+
+ + Ubuntu + MacOS + Windows + + + +

+ +
+ +

+ +
+ +

+
+
+

+ +
+
+ + description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step3.title
+ {{ 'dashboard.dashboards' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step4.title
+ {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step5.title
+
+
+
+

+ +
+ + description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step6.title
+ {{ 'customer.customers' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss index 193fb5e793..20345a97f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss @@ -83,13 +83,105 @@ color: rgba(0, 0, 0, 0.87); } - .tb-get-started .mat-vertical-content p { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.2px; - color: rgba(0, 0, 0, 0.54); + .tb-get-started .mat-vertical-content { + p, li { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + em { + font-style: normal; + font-weight: 500; + font-size: 13px; + color: rgba(0, 0, 0, 0.38); + } + } + ul { + padding-inline-start: 20px; + } + .tb-bordered-content { + display: flex; + flex-direction: column; + align-items: stretch; + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: 8px; + padding: 16px; + p { + margin-top: 16px; + margin-bottom: 8px; + } + } + .tb-markdown-view { + .tb-getting-started-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + padding: 9px 38px 9px 16px; + background: rgba(0, 0, 0, 0.03); + border-radius: 6px; + border: none; + } + code[class*="language-"], pre[class*="language-"] { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.38); + overflow: hidden; + white-space: break-spaces; + word-break: break-all; + & * { + color: rgba(0, 0, 0, 0.38); + cursor: inherit; + background: transparent; + } + } + button.clipboard-btn { + right: 0; + height: 34px; + p, div { + background: transparent; + } + p { + margin: 0; + padding: 7px; + color: #305680; + } + div { + top: 0; + padding: 8px; + height: 34px; + width: 34px; + img { + display: none; + } + &:after { + content: ""; + position: initial; + display: block; + width: 18px; + height: 18px; + background: #305680; + -webkit-mask-image: url(/assets/copy-code-icon.svg); + -webkit-mask-repeat: no-repeat; + mask-image: url(/assets/copy-code-icon.svg); + mask-repeat: no-repeat; + } + } + } + } + } + } + @media #{$mat-md-lg} { + .tb-bordered-content { + padding: 4px; + } + } } @media #{$mat-md-lg} { @@ -107,15 +199,16 @@ line-height: 16px; } - .tb-get-started .mat-vertical-content p { - font-size: 12px; - line-height: 16px; - letter-spacing: 0.25px; + .tb-get-started .mat-vertical-content { + p, li { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.25px; + } } .tb-get-started .mat-vertical-content { padding: 0 16px 16px 16px; } - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts index 9c07e62978..7566ae8579 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts @@ -27,7 +27,7 @@ import { } from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; import { GettingStarted } from '@shared/models/user-settings.models'; import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; -import { isUndefined } from '@core/utils'; +import { baseUrl, isUndefined } from '@core/utils'; import { MatStepper } from '@angular/material/stepper'; import { first } from 'rxjs/operators'; import { Authority } from '@shared/models/authority.enum'; @@ -54,6 +54,8 @@ export class GettingStartedWidgetComponent extends PageComponent implements OnIn }; allCompleted = false; + baseUrl = baseUrl(); + constructor(protected store: Store, private cd: ChangeDetectorRef, private userSettingsService: UserSettingsService, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts index 3be30dad30..debd1d6ca0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts @@ -28,6 +28,7 @@ import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home- import { GettingStartedCompletedDialogComponent } from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; +import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/toggle-header.component'; @NgModule({ declarations: @@ -40,7 +41,8 @@ import { AddDocLinkDialogComponent, EditDocLinksDialogComponent, GettingStartedWidgetComponent, - GettingStartedCompletedDialogComponent + GettingStartedCompletedDialogComponent, + ToggleHeaderComponent ], imports: [ CommonModule, @@ -55,7 +57,8 @@ import { AddDocLinkDialogComponent, EditDocLinksDialogComponent, GettingStartedWidgetComponent, - GettingStartedCompletedDialogComponent + GettingStartedCompletedDialogComponent, + ToggleHeaderComponent ] }) export class HomePageWidgetsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html new file mode 100644 index 0000000000..3cbf61fdae --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html @@ -0,0 +1,20 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss new file mode 100644 index 0000000000..0dce288424 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../scss/constants"; + +:host ::ng-deep { + .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + width: 100%; + border-radius: 100px; + height: 32px; + padding: 2px; + border: none; + background: rgba(0, 0, 0, 0.06); + margin-bottom: 8px; + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + .mat-button-toggle.mat-button-toggle-appearance-standard { + flex: 1; + color: rgba(0, 0, 0, 0.38); + background: transparent; + + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + border-radius: 20px; + } + .mat-button-toggle-button { + height: 28px; + .mat-button-toggle-label-content { + line-height: 26px; + font-weight: 400; + font-size: 14px; + letter-spacing: 0.2px; + } + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + background: #F3F6FA; + color: #305680; + border: 1px solid #305680; + border-radius: 20px; + + .mat-button-toggle-label-content { + font-weight: 500; + line-height: 24px; + } + } + } + } + } + @media #{$mat-md-lg} { + .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + height: 24px; + margin-bottom: 0; + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-button { + height: 20px; + display: grid; + .mat-button-toggle-label-content { + line-height: 20px; + font-size: 10px; + padding: 0 2px; + } + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + .mat-button-toggle-label-content { + line-height: 18px; + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts new file mode 100644 index 0000000000..1d7876eb36 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterContentInit, + AfterViewInit, + ChangeDetectorRef, + Component, + ContentChildren, + Input, + OnInit, + QueryList, + ViewChild +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +import { UpdateMessage } from '@shared/models/settings.models'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { of } from 'rxjs'; +import { MatStepper } from '@angular/material/stepper'; +import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; + +@Component({ + selector: 'tb-toggle-header', + templateUrl: './toggle-header.component.html', + styleUrls: ['./toggle-header.component.scss'] +}) +export class ToggleHeaderComponent extends PageComponent implements OnInit, AfterViewInit { + + @ViewChild('toggleGroup') + toggleGroup: MatButtonToggleGroup; + + @ContentChildren(MatButtonToggle) + _buttonToggles: QueryList; + + innerValue: any; + + @Input() + set value(value: any) { + this.innerValue = value; + } + + get value(): any { + return this.toggleGroup?.value; + } + + @Input() + name: string; + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + } + + ngAfterViewInit() { + for (const toggle of this._buttonToggles) { + toggle.buttonToggleGroup = this.toggleGroup; + if (this.innerValue === toggle.value) { + toggle.checked = true; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index be408da234..3e0f797be1 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -27,6 +27,7 @@ import { AppState } from '@core/core.state'; import { map } from 'rxjs/operators'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import sysAdminHomePageDashboardJson from '!raw-loader!./sys_admin_home_page.raw'; +import tenantAdminHomePageDashboardJson from '!raw-loader!./tenant_admin_home_page.raw'; @Injectable() export class HomeDashboardResolver implements Resolve { @@ -39,8 +40,18 @@ export class HomeDashboardResolver implements Resolve { return this.dashboardService.getHomeDashboard().pipe( map((dashboard) => { if (!dashboard) { - if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { - dashboard = JSON.parse(sysAdminHomePageDashboardJson); + const authority = getCurrentAuthUser(this.store).authority; + switch (authority) { + case Authority.SYS_ADMIN: + dashboard = JSON.parse(sysAdminHomePageDashboardJson); + break; + case Authority.TENANT_ADMIN: + dashboard = JSON.parse(tenantAdminHomePageDashboardJson); + break; + case Authority.CUSTOMER_USER: + break; + } + if (dashboard) { dashboard.hideDashboardToolbar = true; } } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw new file mode 100644 index 0000000000..3044499da0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw @@ -0,0 +1,667 @@ +{ + "title": "Tenant Administrator Home Page", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'widgets.activity.title' | translate }}
\n \n {{ 'device.devices' | translate }}\n {{ 'widgets.transport-messages.title' | translate }}\n \n
\n \n \n \n \n \n \n \n \n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n" + }, + "title": "Transport messages", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" + }, + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "timeseries_bars_flot", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:widgets.transport-messages.title}", + "color": "#305680", + "settings": {}, + "_hash": 0.2880464219129071, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": [] + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": true, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1680443065451, + "endTimeMs": 1680529465451 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "stack": true, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": 0, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "return value % 1000 === 0 ? ((value / 1000) + 'k') : '';" + }, + "defaultBarWidth": 1800000, + "barAlignment": "left", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "Transport messages", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "widgetStyle": { + "padding": "0" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "actions": {}, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", + "pageSize": 1024, + "noDataDisplayMessage": "", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } + }, + "row": 0, + "col": 0, + "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" + }, + "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { + "isSystemType": true, + "bundleAlias": "home_page_widgets", + "typeAlias": "documentation_links", + "type": "static", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 2 + }, + "title": "Documentation", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" + }, + "a23185ad-dc46-806c-0e50-5b21fb080ace": { + "isSystemType": true, + "bundleAlias": "home_page_widgets", + "typeAlias": "getting_started", + "type": "static", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 3 + }, + "title": "Getting started", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "a23185ad-dc46-806c-0e50-5b21fb080ace" + }, + "d26e5cd7-75ef-d475-00c7-1a2d1114efe8": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "basic_timeseries", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "activeDevicesCount", + "type": "timeseries", + "label": "{i18n:device.devices}", + "color": "#305680", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": true, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.9688095820365725, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": [] + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": true, + "hideAggInterval": true, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 7200000, + "fixedTimewindow": { + "startTimeMs": 1681400576338, + "endTimeMs": 1681486976338 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "NONE", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": null, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 0, + "smoothLines": true, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "Devices activity", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": false, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": { + "padding": "0" + }, + "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } + }, + "row": 0, + "col": 0, + "id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8" + } + }, + "states": { + "default": { + "name": "Tenant Administrator Home Page", + "root": true, + "layouts": { + "main": { + "widgets": { + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "sizeX": 41, + "sizeY": 16, + "row": 26, + "col": 44, + "mobileOrder": 7, + "mobileHeight": 8 + }, + "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { + "sizeX": 31, + "sizeY": 16, + "row": 42, + "col": 26, + "mobileHide": true + }, + "a23185ad-dc46-806c-0e50-5b21fb080ace": { + "sizeX": 35, + "sizeY": 58, + "row": 0, + "col": 85, + "mobileHide": true + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 120, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 20, + "outerMargin": true + } + } + } + }, + "transport_messages": { + "name": "Transport messages", + "root": false, + "layouts": { + "main": { + "widgets": { + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#ffffff", + "columns": 24, + "margin": 0, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "outerMargin": true + } + } + } + }, + "devices_activity": { + "name": "Devices activity", + "root": false, + "layouts": { + "main": { + "widgets": { + "d26e5cd7-75ef-d475-00c7-1a2d1114efe8": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#ffffff", + "columns": 24, + "margin": 0, + "outerMargin": true, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70 + } + } + } + } + }, + "entityAliases": { + "ae870700-071f-b3bc-406c-16ba554c5a55": { + "id": "ae870700-071f-b3bc-406c-16ba554c5a55", + "alias": "Tenants", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "ca4d90e4-f9ae-6fca-6b09-85815a48d52b": { + "id": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "alias": "TenantProfiles", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "a1ddb8fa-90ff-5598-e7f2-e254194d055d": { + "id": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "alias": "Devices", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "DEVICE" + } + }, + "619cdf00-a042-3b55-124e-194c1b28c236": { + "id": "619cdf00-a042-3b55-124e-194c1b28c236", + "alias": "Assets", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "ASSET" + } + }, + "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2": { + "id": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "alias": "Users", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "USER" + } + }, + "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31": { + "id": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "alias": "Customers", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "CUSTOMER" + } + }, + "d9229b29-3f46-de8d-7fe8-eb0c43c75079": { + "id": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "alias": "Api Usage State", + "filter": { + "type": "apiUsageState", + "resolveMultiple": true + } + } + }, + "filters": {}, + "timewindow": { + "displayValue": "", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168326072, + "endTimeMs": 1680254726072 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "hideToolbar": true, + "showFilters": true, + "showUpdateDashboardImage": true, + "dashboardCss": ".tb-widget-container > .tb-widget {\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0px 5px 16px rgba(0, 0, 0, 0.04);\n border-radius: 12px;\n}\n\n.tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 16px !important;\n}\n\n.tb-card-title {\n display: grid;\n}\n\n.tb-home-widget-title {\n font-style: normal;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tb-home-widget-link {\n position: relative;\n border-bottom: none;\n}\n\n.tb-home-widget-link:hover {\n border-bottom: none;\n}\n\n.tb-home-widget-link:focus {\n border-bottom: none;\n}\n\n.tb-home-widget-link::after {\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px; \n}\n\n.tb-home-widget-link:hover::after {\n color: inherit;\n}\n\n.tb-home-widget-info-icon {\n color: rgba(0, 0, 0, 0.12);\n font-size: 16px;\n width: 16px;\n height: 16px;\n line-height: 15px;\n vertical-align: middle;\n}\n\n.tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n padding: 0;\n}\n\n.tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n cursor: pointer;\n user-select: none;\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .tb-widget-container > .tb-widget {\n border-radius: 4px;\n }\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 2px !important;\n }\n .tb-hide-md {\n display: none;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 8px !important;\n }\n .tb-hide-lg {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-hide-md-lg {\n display: none;\n }\n\n .tb-home-widget-title {\n font-size: 12px;\n line-height: 16px;\n }\n \n .tb-widget-container > .tb-widget .tb-widget-title {\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 12px;\n line-height: 16px;\n min-height: 24px;\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 {\n width: 24px;\n height: 24px;\n line-height: 24px;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 .mat-icon {\n width: 18px;\n height: 18px;\n font-size: 18px;\n }\n \n .tb-widget-container > .tb-widget tb-legend {\n padding-bottom: 0 !important;\n }\n \n .tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n font-size: 11px;\n line-height: 16px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (max-width: 959px), screen and (min-width: 1820px) {\n .tb-hide-not-md-lg {\n display: none;\n }\n}\n" + } + }, + "externalId": null, + "name": "Tenant Administrator Home Page" +} \ No newline at end of file diff --git a/ui-ngx/src/app/shared/components/markdown.component.html b/ui-ngx/src/app/shared/components/markdown.component.html index 5b8b9f8af0..67e8cd0da2 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.html +++ b/ui-ngx/src/app/shared/components/markdown.component.html @@ -22,5 +22,5 @@ background: #efefef;"> {{error}}
-
+
diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 5af7ccce6b..753effc24f 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -39,6 +39,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { deepClone, guid, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coerce-boolean'; let defaultMarkdownStyle; @@ -76,6 +77,10 @@ export class TbMarkdownComponent implements OnChanges { get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } + @Input() + @coerceBoolean() + usePlainMarkdown = false; + @Output() ready = new EventEmitter(); private lineNumbersValue = false; @@ -124,13 +129,8 @@ export class TbMarkdownComponent implements OnChanges { } template = this.sanitizeCurlyBraces(template); this.markdownContainer.clear(); - const parent = this; - let readyObservable: Observable; - let compileModules = [this.sharedModule]; - if (this.additionalCompileModules) { - compileModules = compileModules.concat(this.additionalCompileModules); - } let styles: string[] = []; + let readyObservable: Observable; if (this.applyDefaultMarkdownStyle) { if (!defaultMarkdownStyle) { defaultMarkdownStyle = deepClone(TbMarkdownComponent['ɵcmp'].styles)[0].replace(/\[_nghost\-%COMP%\]/g, '') @@ -141,70 +141,87 @@ export class TbMarkdownComponent implements OnChanges { if (this.additionalStyles) { styles = styles.concat(this.additionalStyles); } - this.dynamicComponentFactoryService.createDynamicComponentFactory( - class TbMarkdownInstance { - ngOnDestroy(): void { - parent.destroyMarkdownInstanceResources(); - } - }, - template, - compileModules, - true, 1, styles - ).subscribe((factory) => { - this.tbMarkdownInstanceComponentFactory = factory; - const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); - try { - this.tbMarkdownInstanceComponentRef = - this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); - if (this.context) { - for (const propName of Object.keys(this.context)) { - this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; - } - } - this.tbMarkdownInstanceComponentRef.instance.style = this.style; - readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); - this.cd.detectChanges(); - this.error = null; - } catch (error) { - readyObservable = this.handleError(template, error, styles); - } - readyObservable.subscribe(() => { - this.ready.emit(); - }); - }, - (error) => { - readyObservable = this.handleError(template, error, styles); + if (this.usePlainMarkdown) { + readyObservable = this.plainMarkdown(template, styles); this.cd.detectChanges(); readyObservable.subscribe(() => { this.ready.emit(); }); - }); + } else { + const parent = this; + let compileModules = [this.sharedModule]; + if (this.additionalCompileModules) { + compileModules = compileModules.concat(this.additionalCompileModules); + } + this.dynamicComponentFactoryService.createDynamicComponentFactory( + class TbMarkdownInstance { + ngOnDestroy(): void { + parent.destroyMarkdownInstanceResources(); + } + }, + template, + compileModules, + true, 1, styles + ).subscribe((factory) => { + this.tbMarkdownInstanceComponentFactory = factory; + const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); + try { + this.tbMarkdownInstanceComponentRef = + this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); + if (this.context) { + for (const propName of Object.keys(this.context)) { + this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; + } + } + this.tbMarkdownInstanceComponentRef.instance.style = this.style; + readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); + this.cd.detectChanges(); + this.error = null; + } catch (error) { + readyObservable = this.handleError(template, error, styles); + } + readyObservable.subscribe(() => { + this.ready.emit(); + }); + }, + (error) => { + readyObservable = this.handleError(template, error, styles); + this.cd.detectChanges(); + readyObservable.subscribe(() => { + this.ready.emit(); + }); + }); + } } private handleError(template: string, error, styles?: string[]): Observable { this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); this.markdownContainer.clear(); if (this.fallbackToPlainMarkdownValue) { - const element = this.fallbackElement.nativeElement; - let styleElement; - if (styles?.length) { - const markdownClass = 'tb-markdown-view-' + guid(); - let innerStyle = styles.join('\n'); - innerStyle = innerStyle.replace(/\.tb-markdown-view/g, '.' + markdownClass); - template = template.replace(/tb-markdown-view/g, markdownClass); - styleElement = this.renderer.createElement('style'); - styleElement.innerHTML = innerStyle; - } - element.innerHTML = template; - if (styleElement) { - this.renderer.appendChild(element, styleElement); - } - return this.handleImages(element); + return this.plainMarkdown(template, styles); } else { return of(null); } } + private plainMarkdown(template: string, styles?: string[]): Observable { + const element = this.fallbackElement.nativeElement; + let styleElement; + if (styles?.length) { + const markdownClass = 'tb-markdown-view-' + guid(); + let innerStyle = styles.join('\n'); + innerStyle = innerStyle.replace(/\.tb-markdown-view/g, '.' + markdownClass); + template = template.replace(/tb-markdown-view/g, markdownClass); + styleElement = this.renderer.createElement('style'); + styleElement.innerHTML = innerStyle; + } + element.innerHTML = template; + if (styleElement) { + this.renderer.appendChild(element, styleElement); + } + return this.handleImages(element); + } + private handlePlugins(element: HTMLElement): void { if (this.lineNumbers) { this.setPluginClass(element, PrismPlugin.LineNumbers); 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 610cdb209e..0c318d4849 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5114,6 +5114,9 @@ "title": "Transport messages", "info": "All the messages that came from devices" }, + "activity": { + "title": "Activity" + }, "documentation": { "title": "Documentation", "add-link": "Add link", @@ -5201,6 +5204,52 @@ "content": "

Some text

Follow the documentation on how to do it:

", "how-to-configure-notifications": "How to configure Notifications" } + }, + "tenant-admin": { + "step1": { + "title": "Create device", + "content": "

We will manually provision the device using the UI. Follow the documentation on how to do it:

", + "how-to-create-device": "How to create Device" + }, + "step2": { + "title": "Connect device", + "content-before": "

To connect the device you need to get the device credentials. But we recommend using the default auto-generated one.

  • Go to device table
  • Click on the device row to open device details
  • Press the button \"Copy access token\"

Use simple commands to publish data over HTTP:

", + "ubuntu": { + "install-curl": "Install cURL for Ubuntu:" + }, + "macos": { + "install-curl": "Install cURL for MacOS:" + }, + "windows": { + "install-curl": "Starting Windows 10 b17063, cURL is available by default." + }, + "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", + "content-after": "

You can also use other protocols such as MQTT, CoAP, etc.

Follow the documentation on how to do it:

", + "how-to-connect-device": "How to connect Device" + }, + "step3": { + "title": "Create dashboard", + "content": "

Create a dashboard to visualize data from entities such as assets, devices, etc.

Follow the documentation on how to do it:

", + "how-to-create-dashboard": "How to create Dashboard" + }, + "step4": { + "title": "Configure alarm rules", + "alarm-rules": "Alarm rules", + "content": "

When the temperature reaches 25°C, we will raise an alarm. Follow the documentation on how to do it:

", + "how-to-configure-alarm-rules": "How to configure Alarm rules" + }, + "step5": { + "title": "Create alarm", + "content-before": "

To trigger the alarm, send a new telemetry value of 26°C or higher.

", + "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", + "content-after": "

Follow the documentation on how to do it:

", + "how-to-create-alarm": "How to create Alarm" + }, + "step6": { + "title": "Create customer and assign dashboard", + "content": "

By creating end-user dashboards, a customer user can only see his own devices, and data from another customer will be hidden.

Follow the documentation on how to do it:

", + "how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard" + } } } }, From 6bb1fd4f12eeebd63ffcccfbee6e4a977c88b6a4 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Mon, 17 Apr 2023 12:02:26 +0300 Subject: [PATCH 011/103] PROD-1948: bugfixes for delay and deduplication nodes --- .../deduplication/TbMsgDeduplicationNode.java | 12 +- .../rule/engine/delay/TbMsgDelayNode.java | 12 +- .../action/TbMsgDeduplicationNodeTest.java | 402 ------------------ .../transform/TbMsgDeduplicationNodeTest.java | 41 +- 4 files changed, 58 insertions(+), 409 deletions(-) delete mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index e8816ff607..beae54dce6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -43,7 +43,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @RuleNode( - type = ComponentType.ACTION, + type = ComponentType.TRANSFORMATION, name = "deduplication", configClazz = TbMsgDeduplicationNodeConfiguration.class, nodeDescription = "Deduplicate messages for a configurable period based on a specified deduplication strategy.", @@ -153,7 +153,15 @@ public class TbMsgDeduplicationNode implements TbNode { } } } - deduplicationResults.add(resultMsg); + if (resultMsg != null) { + deduplicationResults.add(TbMsg.newMsg( + resultMsg.getQueueName(), + resultMsg.getType(), + resultMsg.getOriginator(), + resultMsg.getCustomerId(), + resultMsg.getMetaData(), + resultMsg.getData())); + } } packBoundsOpt = findValidPack(msgList, deduplicationTimeoutMs); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 857a068e8c..1f18aea86e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -66,7 +66,17 @@ public class TbMsgDelayNode implements TbNode { if (msg.getType().equals(TB_MSG_DELAY_NODE_MSG)) { TbMsg pendingMsg = pendingMsgs.remove(UUID.fromString(msg.getData())); if (pendingMsg != null) { - ctx.enqueueForTellNext(pendingMsg, SUCCESS); + ctx.enqueueForTellNext( + TbMsg.newMsg( + pendingMsg.getQueueName(), + pendingMsg.getType(), + pendingMsg.getOriginator(), + pendingMsg.getCustomerId(), + pendingMsg.getMetaData(), + pendingMsg.getData() + ), + SUCCESS + ); } } else { if (pendingMsgs.size() < config.getMaxPendingMsgs()) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java deleted file mode 100644 index 1091a7d29c..0000000000 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java +++ /dev/null @@ -1,402 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.rule.engine.action; - -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.stubbing.Answer; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; -import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; -import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; -import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@Slf4j -public class TbMsgDeduplicationNodeTest { - - private static final String MAIN_QUEUE_NAME = "Main"; - private static final String HIGH_PRIORITY_QUEUE_NAME = "HighPriority"; - private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; - - private TbContext ctx; - - private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); - private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(factory); - private final int deduplicationInterval = 1; - - private TenantId tenantId; - - private TbMsgDeduplicationNode node; - private TbMsgDeduplicationNodeConfiguration config; - private TbNodeConfiguration nodeConfiguration; - - private CountDownLatch awaitTellSelfLatch; - - @BeforeEach - public void init() throws TbNodeException { - ctx = mock(TbContext.class); - - tenantId = TenantId.fromUUID(UUID.randomUUID()); - RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID()); - - when(ctx.getSelfId()).thenReturn(ruleNodeId); - when(ctx.getTenantId()).thenReturn(tenantId); - - doAnswer((Answer) invocationOnMock -> { - String type = (String) (invocationOnMock.getArguments())[1]; - EntityId originator = (EntityId) (invocationOnMock.getArguments())[2]; - TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3]; - String data = (String) (invocationOnMock.getArguments())[4]; - return TbMsg.newMsg(type, originator, metaData.copy(), data); - }).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); - node = spy(new TbMsgDeduplicationNode()); - config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration(); - } - - private void invokeTellSelf(int maxNumberOfInvocation) { - invokeTellSelf(maxNumberOfInvocation, false, 0); - } - - private void invokeTellSelf(int maxNumberOfInvocation, boolean delayScheduleTimeout, int delayMultiplier) { - AtomicLong scheduleTimeout = new AtomicLong(deduplicationInterval); - AtomicInteger scheduleCount = new AtomicInteger(0); - doAnswer((Answer) invocationOnMock -> { - scheduleCount.getAndIncrement(); - if (scheduleCount.get() <= maxNumberOfInvocation) { - TbMsg msg = (TbMsg) (invocationOnMock.getArguments())[0]; - executorService.schedule(() -> { - try { - node.onMsg(ctx, msg); - awaitTellSelfLatch.countDown(); - } catch (ExecutionException | InterruptedException | TbNodeException e) { - log.error("Failed to execute tellSelf method call due to: ", e); - } - }, scheduleTimeout.get(), TimeUnit.SECONDS); - if (delayScheduleTimeout) { - scheduleTimeout.set(scheduleTimeout.get() * delayMultiplier); - } - } - - return null; - }).when(ctx).tellSelf(ArgumentMatchers.any(TbMsg.class), ArgumentMatchers.anyLong()); - } - - @AfterEach - public void destroy() { - executorService.shutdown(); - node.destroy(); - } - - @Test - public void given_100_messages_strategy_first_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation); - - config.setInterval(deduplicationInterval); - config.setMaxPendingMsgs(msgCount); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); - for (TbMsg msg : inputMsgs) { - node.onMsg(ctx, msg); - } - - TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); - node.onMsg(ctx, msgToReject); - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); - } - - @Test - public void given_100_messages_strategy_last_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation); - - config.setStrategy(DeduplicationStrategy.LAST); - config.setInterval(deduplicationInterval); - config.setMaxPendingMsgs(msgCount); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); - TbMsg msgWithLatestTs = getMsgWithLatestTs(inputMsgs); - - for (TbMsg msg : inputMsgs) { - node.onMsg(ctx, msg); - } - - TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); - node.onMsg(ctx, msgToReject); - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); - } - - @Test - public void given_100_messages_strategy_all_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation); - - config.setInterval(deduplicationInterval); - config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); - config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); - for (TbMsg msg : inputMsgs) { - node.onMsg(ctx, msg); - } - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - - Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); - TbMsg outMessage = newMsgCaptor.getAllValues().get(0); - Assertions.assertEquals(getMergedData(inputMsgs), outMessage.getData()); - Assertions.assertEquals(deviceId, outMessage.getOriginator()); - Assertions.assertEquals(config.getOutMsgType(), outMessage.getType()); - Assertions.assertEquals(config.getQueueName(), outMessage.getQueueName()); - } - - @Test - public void given_100_messages_strategy_all_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); - - config.setInterval(deduplicationInterval); - config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); - config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); - for (TbMsg msg : firstMsgPack) { - node.onMsg(ctx, msg); - } - long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); - - List secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); - for (TbMsg msg : secondMsgPack) { - node.onMsg(ctx, msg); - } - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - - List resultMsgs = newMsgCaptor.getAllValues(); - Assertions.assertEquals(2, resultMsgs.size()); - - TbMsg firstMsg = resultMsgs.get(0); - Assertions.assertEquals(getMergedData(firstMsgPack), firstMsg.getData()); - Assertions.assertEquals(deviceId, firstMsg.getOriginator()); - Assertions.assertEquals(config.getOutMsgType(), firstMsg.getType()); - Assertions.assertEquals(config.getQueueName(), firstMsg.getQueueName()); - - TbMsg secondMsg = resultMsgs.get(1); - Assertions.assertEquals(getMergedData(secondMsgPack), secondMsg.getData()); - Assertions.assertEquals(deviceId, secondMsg.getOriginator()); - Assertions.assertEquals(config.getOutMsgType(), secondMsg.getType()); - Assertions.assertEquals(config.getQueueName(), secondMsg.getQueueName()); - } - - @Test - public void given_100_messages_strategy_last_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); - - config.setInterval(deduplicationInterval); - config.setStrategy(DeduplicationStrategy.LAST); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); - for (TbMsg msg : firstMsgPack) { - node.onMsg(ctx, msg); - } - long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); - TbMsg msgWithLatestTsInFirstPack = getMsgWithLatestTs(firstMsgPack); - - List secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); - for (TbMsg msg : secondMsgPack) { - node.onMsg(ctx, msg); - } - TbMsg msgWithLatestTsInSecondPack = getMsgWithLatestTs(secondMsgPack); - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - - List resultMsgs = newMsgCaptor.getAllValues(); - Assertions.assertEquals(2, resultMsgs.size()); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); - } - - private TbMsg getMsgWithLatestTs(List firstMsgPack) { - int indexOfLastMsgInArray = firstMsgPack.size() - 1; - int indexToSetMaxTs = new Random().nextInt(indexOfLastMsgInArray) + 1; - TbMsg currentMaxTsMsg = firstMsgPack.get(indexOfLastMsgInArray); - TbMsg newLastMsgOfArray = firstMsgPack.get(indexToSetMaxTs); - firstMsgPack.set(indexOfLastMsgInArray, newLastMsgOfArray); - firstMsgPack.set(indexToSetMaxTs, currentMaxTsMsg); - return currentMaxTsMsg; - } - - private List getTbMsgs(DeviceId deviceId, int msgCount, long currentTimeMillis, int initTsStep) { - List inputMsgs = new ArrayList<>(); - var ts = currentTimeMillis + initTsStep; - for (int i = 0; i < msgCount; i++) { - inputMsgs.add(createMsg(deviceId, ts)); - ts += 2; - } - return inputMsgs; - } - - private TbMsg createMsg(DeviceId deviceId, long ts) { - ObjectNode dataNode = JacksonUtil.newObjectNode(); - dataNode.put("deviceId", deviceId.getId().toString()); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("ts", String.valueOf(ts)); - return TbMsg.newMsg( - MAIN_QUEUE_NAME, - SessionMsgType.POST_TELEMETRY_REQUEST.name(), - deviceId, - metaData, - JacksonUtil.toString(dataNode)); - } - - private String getMergedData(List msgs) { - ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode(); - msgs.forEach(msg -> { - ObjectNode msgNode = JacksonUtil.newObjectNode(); - msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData())); - msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData())); - mergedData.add(msgNode); - }); - return JacksonUtil.toString(mergedData); - } - -} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java index 475a98e2a2..1badecfa92 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java @@ -174,7 +174,16 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); + + TbMsg firstMsg = inputMsgs.get(0); + TbMsg actualMsg = newMsgCaptor.getValue(); + // msg ids should be different because we create new msg before enqueueForTellNext + Assertions.assertNotEquals(firstMsg.getId(), actualMsg.getId()); + Assertions.assertEquals(firstMsg.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(firstMsg.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(firstMsg.getData(), actualMsg.getData()); + Assertions.assertEquals(firstMsg.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(firstMsg.getType(), actualMsg.getType()); } @Test @@ -213,7 +222,15 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); + + TbMsg actualMsg = newMsgCaptor.getValue(); + // msg ids should be different because we create new msg before enqueueForTellNext + Assertions.assertNotEquals(msgWithLatestTs.getId(), actualMsg.getId()); + Assertions.assertEquals(msgWithLatestTs.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(msgWithLatestTs.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(msgWithLatestTs.getData(), actualMsg.getData()); + Assertions.assertEquals(msgWithLatestTs.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(msgWithLatestTs.getType(), actualMsg.getType()); } @Test @@ -350,8 +367,24 @@ public class TbMsgDeduplicationNodeTest { List resultMsgs = newMsgCaptor.getAllValues(); Assertions.assertEquals(2, resultMsgs.size()); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); + + // verify that newMsg is called but content of messages is the same as in the last msg for the first pack. + TbMsg actualMsg = resultMsgs.get(0); + Assertions.assertNotEquals(msgWithLatestTsInFirstPack.getId(), actualMsg.getId()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getData(), actualMsg.getData()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getType(), actualMsg.getType()); + + // verify that newMsg is called but content of messages is the same as in the last msg for the second pack. + actualMsg = resultMsgs.get(1); + Assertions.assertNotEquals(msgWithLatestTsInSecondPack.getId(), actualMsg.getId()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getData(), actualMsg.getData()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getType(), actualMsg.getType()); } private TbMsg getMsgWithLatestTs(List firstMsgPack) { From 64264a7834c16427af80e9bb82193a503eb584dc Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 17 Apr 2023 12:07:03 +0300 Subject: [PATCH 012/103] UI: Refactoring color picker --- .../color-picker/color-picker.component.html | 30 ++--- .../color-picker/color-picker.component.scss | 52 ++------- .../color-picker/color-picker.component.ts | 109 ++++++++---------- .../dialog/color-picker-dialog.component.html | 6 +- .../dialog/color-picker-dialog.component.ts | 8 -- 5 files changed, 73 insertions(+), 132 deletions(-) diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index 1005fa4a58..b5082530cd 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -17,29 +17,21 @@ --> -
-
-
- -
-
+
+ +
-
-
-
- - - - - -
-
- -
+
+
+ + + +
+
+
-
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index 38b55346aa..ddd870c7d0 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -14,17 +14,17 @@ * limitations under the License. */ :host { - display: block; width: 100%; - max-width: 320px; - min-width: 100%; - border-radius: 2px; - background: #fff; + display: flex; + flex-direction: column; ::ng-deep { .saturation-component { - height: 200px; + height: 100%; + min-height: 200px; + max-height: 300px; border-radius: 8px; + margin-bottom: 16px; div.pointer { border-width: 2px; width: 16px; @@ -56,25 +56,9 @@ } } - - -.controls { - padding: 15px 0; -} - -.controls-row { - display: table; - width: 100%; -} - -.column { - display: table-cell; - vertical-align: middle; -} - -.hue-alpha .column:first-child { - width: 42px; - padding: 0 10px 0 0; +.control-component { + max-height: 48px; + margin-bottom: 16px; } indicator-component { @@ -90,17 +74,8 @@ indicator-component { } } -hue-component { - margin-bottom: 16px; -} - color-presets-component { border-top: 1px solid #d0d0d0; - padding: 12px; - - ::ng-deep .presets-row { - padding: 12px 0 0; - } } .type-btn { @@ -114,12 +89,3 @@ color-presets-component { background-color: #eee; } } - -.type-column { - width: 25px; - text-align: right; -} - -.presentation { - padding: 12px 0 0; -} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts index ab8fb847dd..dd7afee70a 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts @@ -14,20 +14,10 @@ /// limitations under the License. /// -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - SimpleChanges -} from '@angular/core'; +import { Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; import { Color, ColorPickerControl } from '@iplab/ngx-color-picker'; import { Subscription } from 'rxjs'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; export enum ColorType { hex = 'hex', @@ -43,89 +33,90 @@ export enum ColorType { selector: `tb-color-picker`, templateUrl: `./color-picker.component.html`, styleUrls: [`./color-picker.component.scss`], - changeDetection: ChangeDetectionStrategy.OnPush + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorPickerComponent), + multi: true + } + ] }) -export class ColorPickerComponent implements OnInit, OnChanges, OnDestroy { - - public selectedPresentation = 0; - public presentations = [ColorType.hex, ColorType.rgb, ColorType.rgba, ColorType.hsla, ColorType.hsl]; +export class ColorPickerComponent implements OnInit, ControlValueAccessor, OnDestroy { - @Input() - public color: string; + selectedPresentation = 0; + presentations = [ColorType.hex, ColorType.rgba, ColorType.hsla]; + control = new ColorPickerControl(); - @Input() - public control: ColorPickerControl; - - @Output() - public colorChange: EventEmitter = new EventEmitter(false); + private modelValue: string; private subscriptions: Array = []; - constructor(private readonly cdr: ChangeDetectorRef) { + private propagateChange = null; + + constructor() { } public ngOnInit(): void { - if (!this.control) { - this.control = new ColorPickerControl(); - } + this.subscriptions.push( + this.control.valueChanges.subscribe(value => { + if (this.modelValue) { + this.updateModel(); + } + }) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + writeValue(value: string): void { + this.control. setValueFrom(value || '#fff'); + this.modelValue = value; if (this.control.initType === ColorType.hexa) { this.control.initType = ColorType.hex; + } else if (this.control.initType === ColorType.rgb) { + this.control.initType = ColorType.rgba; + } else if (this.control.initType === ColorType.hsl) { + this.control.initType = ColorType.hsla; } this.selectedPresentation = this.presentations.indexOf(this.control.initType); - - if (this.color) { - this.control.setValueFrom(this.color); - } - - this.subscriptions.push( - this.control.valueChanges.subscribe((value) => { - this.cdr.markForCheck(); - this.colorChange.emit(this.getValueByType(value, this.presentations[this.selectedPresentation])); - }) - ); } - changeColorFormat(event: Event) { - this.colorChange.emit(this.getValueByType(this.control.value, this.presentations[this.selectedPresentation])); + private updateModel() { + const color: string = this.getValueByType(this.control.value, this.presentations[this.selectedPresentation]); + if (this.modelValue !== color) { + this.modelValue = color; + this.propagateChange(color); + } } public ngOnDestroy(): void { - this.cdr.detach(); this.subscriptions.forEach((subscription) => subscription.unsubscribe()); this.subscriptions.length = 0; } - public ngOnChanges(changes: SimpleChanges): void { - if (this.color && this.control && - this.getValueByType(this.control.value, this.presentations[this.selectedPresentation]) !== this.color) { - this.control.setValueFrom(this.color); - } - } - public changePresentation(): void { this.selectedPresentation = this.selectedPresentation === this.presentations.length - 1 ? 0 : this.selectedPresentation + 1; - this.colorChange.emit(this.getValueByType(this.control.value, this.presentations[this.selectedPresentation])); - this.cdr.markForCheck(); + this.updateModel(); } getValueByType(color: Color, type: ColorType): string { switch (type) { - case ColorType.hex || ColorType.hexa: + case ColorType.hex: return color.toHexString(this.control.value.getRgba().getAlpha() !== 1); - case ColorType.rgb: - return color.toRgbString(); case ColorType.rgba: - return color.toRgbaString(); - case ColorType.hsl: - return color.toHslString(); + return this.control.value.getRgba().getAlpha() !== 1 ? color.toRgbaString() : color.toRgbString(); case ColorType.hsla: - return color.toHslaString(); + return this.control.value.getRgba().getAlpha() !== 1 ? color.toHslaString() : color.toHslString(); default: return color.toRgbaString(); } } - } diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index aedeff35b2..de733b2a20 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -15,9 +15,9 @@ limitations under the License. --> - -
- + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 8a9d5cbdf8..0938db05db 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -29,7 +29,6 @@ import { } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { ColorPickerControl } from '@iplab/ngx-color-picker'; export interface ColorPickerDialogData { color: string; @@ -48,8 +47,6 @@ export class ColorPickerDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, @@ -71,11 +68,6 @@ export class ColorPickerDialogComponent extends DialogComponent Date: Mon, 17 Apr 2023 14:01:15 +0300 Subject: [PATCH 013/103] UI: Refactoring color picker --- .../color-picker/color-picker.component.html | 34 ++--- .../color-picker/color-picker.component.scss | 116 ++++++++++-------- .../color-picker/color-picker.component.ts | 20 +-- .../dialog/color-picker-dialog.component.html | 4 +- .../dialog/color-picker-dialog.component.ts | 29 +---- 5 files changed, 101 insertions(+), 102 deletions(-) diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index b5082530cd..d1432d27f5 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -17,21 +17,25 @@ --> -
- -
- - -
+
+ + +
+ +
+
-
-
- - - -
-
- -
+
+
+ + +
+
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index ddd870c7d0..6d3978550a 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -17,75 +17,87 @@ width: 100%; display: flex; flex-direction: column; + gap: 16px; - ::ng-deep { - .saturation-component { - height: 100%; - min-height: 200px; - max-height: 300px; - border-radius: 8px; - margin-bottom: 16px; - div.pointer { - border-width: 2px; - width: 16px; - height: 16px; - } - } + .saturation-component { + height: 100%; + min-height: 200px; + max-height: 300px; + border-radius: 8px; + } - indicator-component { - svg { - vertical-align: -70% !important; - } + .control-component { + max-height: 48px; + display: flex; + gap: 16px; + + .indicator-component { + height: 48px; + width: 48px; + border-radius: 8px; + background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMUAFg9Gm0AAAAPSURBVAjXY2D4jxXhEgYAfr8P8QhChVEAAAAASUVORK5CYII=') repeat; } .hue-alpha-range { - alpha-component, hue-component { + display: flex; + justify-content: space-between; + flex-direction: column; + flex: 1; + + > * { height: 18px; + border-radius: 9px; border: 1px solid rgba(0, 0, 0, 0.1); - .pointer { - height: 18px; - width: 18px; - background: none; - border: 2px solid #fff; - } - .gradient-color { - border-radius: 24px; - } } } } -} -.control-component { - max-height: 48px; - margin-bottom: 16px; -} + .color-input-block { + display: flex; -indicator-component { - height: 48px; - width: 48px; - border-radius: 8px; - background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMUAFg9Gm0AAAAPSURBVAjXY2D4jxXhEgYAfr8P8QhChVEAAAAASUVORK5CYII=') repeat; -} + .color-input { + flex: 1; + } -.hue-alpha-range { - hue-component, alpha-component { - border-radius: 24px; + .type-btn { + height: 26px; + width: 20px; + background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAMAAAAootjDAAAAM1BMVEUAAAAzMzMzMzMzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzMyMjIrKyszMzPF8UZlAAAAEHRSTlMA1fHr4ZxxSRP45sG+sCkGH2+Z6QAAAHJJREFUKM+9kkkSgCAQA0FEVLb5/2tViqgQvNrHviSzKGCt6nDGuNass8i8NsrLiX+bZbrUtDwm7VLYE0zWUtEZ+RvUZpEvN8YhH9QmQRoC8kFpEnVHVP/DJUZVeSAem5fDKxwtms/BR+PT8gN8vwk/0wE1gQzNVYryIwAAAABJRU5ErkJggg==') no-repeat center; + background-size: 6px 12px; + + &:hover { + background-color: #eee; + } + } } } -color-presets-component { - border-top: 1px solid #d0d0d0; -} +:host ::ng-deep { + .saturation-component { + .pointer { + border-width: 2px; + width: 16px; + height: 16px; + } + } -.type-btn { - display: inline-block; - height: 20px; - width: 20px; - background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAMAAAAootjDAAAAM1BMVEUAAAAzMzMzMzMzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzMyMjIrKyszMzPF8UZlAAAAEHRSTlMA1fHr4ZxxSRP45sG+sCkGH2+Z6QAAAHJJREFUKM+9kkkSgCAQA0FEVLb5/2tViqgQvNrHviSzKGCt6nDGuNass8i8NsrLiX+bZbrUtDwm7VLYE0zWUtEZ+RvUZpEvN8YhH9QmQRoC8kFpEnVHVP/DJUZVeSAem5fDKxwtms/BR+PT8gN8vwk/0wE1gQzNVYryIwAAAABJRU5ErkJggg==') no-repeat center; - background-size: 6px 12px; + indicator-component { + svg { + vertical-align: -70% !important; + } + } - &:hover { - background-color: #eee; + .hue-alpha-range { + alpha-component, hue-component { + .pointer { + height: 18px; + width: 18px; + background: none; + border: 2px solid #fff; + } + .gradient-color { + border-radius: 9px; + } + } } } diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts index dd7afee70a..ce49310b03 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; +import { Component, forwardRef, OnDestroy } from '@angular/core'; import { Color, ColorPickerControl } from '@iplab/ngx-color-picker'; import { Subscription } from 'rxjs'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -41,7 +41,7 @@ export enum ColorType { } ] }) -export class ColorPickerComponent implements OnInit, ControlValueAccessor, OnDestroy { +export class ColorPickerComponent implements ControlValueAccessor, OnDestroy { selectedPresentation = 0; presentations = [ColorType.hex, ColorType.rgba, ColorType.hsla]; @@ -51,16 +51,17 @@ export class ColorPickerComponent implements OnInit, ControlValueAccessor, OnDes private subscriptions: Array = []; - private propagateChange = null; + private propagateChange = (v: any) => {}; - constructor() { - } + private setValue = false; - public ngOnInit(): void { + constructor() { this.subscriptions.push( - this.control.valueChanges.subscribe(value => { - if (this.modelValue) { + this.control.valueChanges.subscribe(() => { + if (!this.setValue) { this.updateModel(); + } else { + this.setValue = false; } }) ); @@ -74,7 +75,8 @@ export class ColorPickerComponent implements OnInit, ControlValueAccessor, OnDes } writeValue(value: string): void { - this.control. setValueFrom(value || '#fff'); + this.setValue = !!value; + this.control.setValueFrom(value || '#fff'); this.modelValue = value; if (this.control.initType === ColorType.hexa) { diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index de733b2a20..eaec4c0b5e 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -15,8 +15,8 @@ limitations under the License. --> - -
+ +
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 0938db05db..44959023f4 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -14,19 +14,11 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { - FormGroupDirective, - NgForm, - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - Validators -} from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; @@ -37,22 +29,18 @@ export interface ColorPickerDialogData { @Component({ selector: 'tb-color-picker-dialog', templateUrl: './color-picker-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ColorPickerDialogComponent}], styleUrls: [] }) export class ColorPickerDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { + implements OnInit { - colorPickerFormGroup: UntypedFormGroup; - - submitted = false; + colorPickerFormGroup: FormGroup; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder,) { + public fb: FormBuilder) { super(store, router, dialogRef); } @@ -62,18 +50,11 @@ export class ColorPickerDialogComponent extends DialogComponent Date: Mon, 17 Apr 2023 16:24:32 +0300 Subject: [PATCH 014/103] UI: Fixed hue-alpha-range overflow --- .../shared/components/color-picker/color-picker.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index 6d3978550a..f71707195a 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -46,6 +46,7 @@ > * { height: 18px; + overflow: hidden; border-radius: 9px; border: 1px solid rgba(0, 0, 0, 0.1); } From a5cfe079996e53d38b3a57038c7013915878a00f Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 17 Apr 2023 16:36:33 +0300 Subject: [PATCH 015/103] Edge - improved logic of interruption of sending downlink tasks --- .../service/edge/rpc/EdgeGrpcService.java | 2 +- .../service/edge/rpc/EdgeGrpcSession.java | 58 +++++++++---------- .../service/edge/rpc/EdgeSessionState.java | 2 +- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index f273517b74..5171a6037f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -288,7 +288,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i if (session != null) { boolean success = false; if (session.isConnected()) { - session.startSyncProcess(tenantId, edgeId, true); + session.startSyncProcess(true); success = true; } clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 9e3a22f64f..33aed92682 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -148,7 +148,7 @@ public final class EdgeGrpcSession implements Closeable { if (requestMsg.getSyncRequestMsg().hasFullSync()) { fullSync = requestMsg.getSyncRequestMsg().getFullSync(); } - startSyncProcess(edge.getTenantId(), edge.getId(), fullSync); + startSyncProcess(fullSync); } else { syncCompleted = true; } @@ -192,10 +192,10 @@ public final class EdgeGrpcSession implements Closeable { }; } - public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) { - log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId); + public void startSyncProcess(boolean fullSync) { + log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId); syncCompleted = false; - interruptGeneralProcessingOnSync(tenantId, edgeId); + interruptGeneralProcessingOnSync(); doSync(new EdgeSyncCursor(ctx, edge, fullSync)); } @@ -221,9 +221,9 @@ public final class EdgeGrpcSession implements Closeable { .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) .build(); - Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback() { + Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { @Override - public void onSuccess(Void result) { + public void onSuccess(Boolean isInterrupted) { syncCompleted = true; ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); } @@ -272,7 +272,7 @@ public final class EdgeGrpcSession implements Closeable { } if (sessionState.getPendingMsgsMap().isEmpty()) { log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); @@ -367,14 +367,19 @@ public final class EdgeGrpcSession implements Closeable { if (isConnected() && !pageData.getData().isEmpty()) { log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); - Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback() { + Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { @Override - public void onSuccess(@Nullable Void tmp) { - if (isConnected() && pageData.hasNext()) { - processEdgeEvents(fetcher, pageLink.nextPageLink(), result); + public void onSuccess(@Nullable Boolean isInterrupted) { + if (Boolean.TRUE.equals(isInterrupted)) { + log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId); + result.set(null); } else { - UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); - result.set(ifOffset); + if (isConnected() && pageData.hasNext()) { + processEdgeEvents(fetcher, pageLink.nextPageLink(), result); + } else { + UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); + result.set(ifOffset); + } } } @@ -394,7 +399,7 @@ public final class EdgeGrpcSession implements Closeable { } } - private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { + private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { interruptPreviousSendDownlinkMsgsTask(); sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); @@ -433,14 +438,14 @@ public final class EdgeGrpcSession implements Closeable { } else { log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } else { - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e); - stopCurrentSendDownlinkMsgsTask(e); + stopCurrentSendDownlinkMsgsTask(true); } }; @@ -688,23 +693,18 @@ public final class EdgeGrpcSession implements Closeable { } private void interruptPreviousSendDownlinkMsgsTask() { - String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId); - stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); } - private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) { - String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId); - stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); + private void interruptGeneralProcessingOnSync() { + log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); } - public void stopCurrentSendDownlinkMsgsTask(Exception e) { + public void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { - if (e != null) { - log.debug(e.getMessage()); - sessionState.getSendDownlinkMsgsFuture().setException(e); - } else { - sessionState.getSendDownlinkMsgsFuture().set(null); - } + sessionState.getSendDownlinkMsgsFuture().set(isInterrupted); } if (sessionState.getScheduledSendDownlinkTask() != null) { sessionState.getScheduledSendDownlinkTask().cancel(true); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java index 59910c829f..09a7c5add8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java @@ -28,6 +28,6 @@ import java.util.concurrent.ScheduledFuture; public class EdgeSessionState { private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); - private SettableFuture sendDownlinkMsgsFuture; + private SettableFuture sendDownlinkMsgsFuture; private ScheduledFuture scheduledSendDownlinkTask; } From 9c1e57a8c134a7db86cdbc177054fcb45e5e9d6f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 17 Apr 2023 18:26:11 +0300 Subject: [PATCH 016/103] Add correct validation message for X.509 certificate in Device Profile --- .../server/dao/device/DeviceProfileServiceImpl.java | 5 ++++- .../device-profile-provision-configuration.component.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index a5b3e56341..391695f2bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -160,9 +160,12 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService Date: Mon, 17 Apr 2023 19:09:23 +0300 Subject: [PATCH 017/103] UI: Tenant admin home page --- .../widget_bundles/home_page_widgets.json | 48 ++++- ui-ngx/src/app/core/http/public-api.ts | 1 + .../src/app/core/http/usage-info.service.ts | 37 ++++ .../app/core/http/user-settings.service.ts | 12 +- ui-ngx/src/app/core/services/menu.service.ts | 168 +++++++++++------- .../widget/lib/flot-widget.models.ts | 1 + .../home/components/widget/lib/flot-widget.ts | 2 +- .../configured-features.component.scss | 14 -- .../configured-features.component.ts | 2 +- .../home-page/doc-links-widget.component.html | 16 +- .../home-page/doc-links-widget.component.ts | 6 +- .../getting-started-widget.component.html | 19 +- .../lib/home-page/home-page-widget.scss | 70 ++++++++ .../lib/home-page/home-page-widgets.module.ts | 10 +- ...onent.scss => links-widget.component.scss} | 52 +----- .../quick-links-widget.component.html | 51 ++++++ .../home-page/quick-links-widget.component.ts | 162 +++++++++++++++++ .../home-page/toggle-header.component.html | 11 +- .../home-page/toggle-header.component.scss | 51 +++++- .../lib/home-page/toggle-header.component.ts | 57 +++--- .../usage-info-widget.component.html | 89 ++++++++++ .../usage-info-widget.component.scss | 108 +++++++++++ .../home-page/usage-info-widget.component.ts | 111 ++++++++++++ .../lib/home-page/version-info.component.scss | 18 -- .../lib/home-page/version-info.component.ts | 2 +- .../chart/flot-key-settings.component.html | 6 +- .../chart/flot-key-settings.component.ts | 20 ++- ...quick-links-widget-settings.component.html | 23 +++ .../quick-links-widget-settings.component.ts | 52 ++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../home-links/tenant_admin_home_page.raw | 89 +++++++++- ui-ngx/src/app/shared/models/public-api.ts | 1 + ui-ngx/src/app/shared/models/usage.models.ts | 39 ++++ .../app/shared/models/user-settings.models.ts | 4 + .../src/app/shared/pipe/short-number.pipe.ts | 53 ++++++ ui-ngx/src/app/shared/shared.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 11 ++ 37 files changed, 1212 insertions(+), 223 deletions(-) create mode 100644 ui-ngx/src/app/core/http/usage-info.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss rename ui-ngx/src/app/modules/home/components/widget/lib/home-page/{doc-links-widget.component.scss => links-widget.component.scss} (73%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts create mode 100644 ui-ngx/src/app/shared/models/usage.models.ts create mode 100644 ui-ngx/src/app/shared/pipe/short-number.pipe.ts diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index fc5b13154c..62eb3d4d6e 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -8,6 +8,25 @@ "name": "Home page widgets" }, "widgetTypes": [ + { + "alias": "getting_started", + "name": "Getting started", + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}" + } + }, { "alias": "documentation_links", "name": "Documentation links", @@ -24,12 +43,12 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-doc-links-widget-settings", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true}" + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" } }, { - "alias": "getting_started", - "name": "Getting started", + "alias": "usage_info", + "name": "Usage info", "image": null, "description": null, "descriptor": { @@ -37,13 +56,32 @@ "sizeX": 7.5, "sizeY": 6.5, "resources": [], - "templateHtml": "\n\n", + "templateHtml": "\n\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}" + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Usage info\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + } + }, + { + "alias": "quick_links", + "name": "Quick links", + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-quick-links-widget-settings", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Quick links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" } } ] diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index 5a511b8f60..d96df82055 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -42,3 +42,4 @@ export * from './ui-settings.service'; export * from './user.service'; export * from './user-settings.service'; export * from './widget.service'; +export * from './usage-info.service'; diff --git a/ui-ngx/src/app/core/http/usage-info.service.ts b/ui-ngx/src/app/core/http/usage-info.service.ts new file mode 100644 index 0000000000..a0869c5dba --- /dev/null +++ b/ui-ngx/src/app/core/http/usage-info.service.ts @@ -0,0 +1,37 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { UsageInfo } from '@shared/models/usage.models'; + +@Injectable({ + providedIn: 'root' +}) +export class UsageInfoService { + + constructor( + private http: HttpClient + ) {} + + public getUsageInfo(config?: RequestConfig): Observable { + return this.http.get('/api/usage', + defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/user-settings.service.ts b/ui-ngx/src/app/core/http/user-settings.service.ts index 55ce524750..ed6fc42a0c 100644 --- a/ui-ngx/src/app/core/http/user-settings.service.ts +++ b/ui-ngx/src/app/core/http/user-settings.service.ts @@ -19,7 +19,7 @@ import { Observable } from 'rxjs'; import { DocumentationLink, DocumentationLinks, GettingStarted, - initialUserSettings, + initialUserSettings, QuickLinks, UserSettings, UserSettingsType } from '@shared/models/user-settings.models'; @@ -67,6 +67,16 @@ export class UserSettingsService { defaultHttpOptionsFromConfig(config)); } + public getQuickLinks(config?: RequestConfig): Observable { + return this.http.get(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`, + defaultHttpOptionsFromConfig(config)); + } + + public updateQuickLinks(quickLinks: QuickLinks, config?: RequestConfig): Observable { + return this.http.put(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`, quickLinks, + defaultHttpOptionsFromConfig(config)); + } + public getGettingStarted(config?: RequestConfig): Observable { return this.http.get(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`, defaultHttpOptionsFromConfig(config)); diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 82ad8221fa..99f4a557a7 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -18,11 +18,10 @@ import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; -import { filter, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { HomeSection, MenuSection } from '@core/services/menu.models'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { Authority } from '@shared/models/authority.enum'; -import { guid } from '@core/utils'; import { AuthState } from '@core/auth/auth.models'; import { NavigationEnd, Router } from '@angular/router'; @@ -34,6 +33,9 @@ export class MenuService { currentMenuSections: Array; menuSections$: Subject> = new BehaviorSubject>([]); homeSections$: Subject> = new BehaviorSubject>([]); + availableMenuLinks$ = this.menuSections$.pipe( + map((items) => this.allMenuLinks(items)) + ); constructor(private store: Store, private router: Router) { @@ -91,21 +93,21 @@ export class MenuService { const sections: Array = []; sections.push( { - id: guid(), + id: 'home', name: 'home.home', type: 'link', path: '/home', icon: 'home' }, { - id: guid(), + id: 'tenants', name: 'tenant.tenants', type: 'link', path: '/tenants', icon: 'supervisor_account' }, { - id: guid(), + id: 'tenantProfiles', name: 'tenant-profile.tenant-profiles', type: 'link', path: '/tenantProfiles', @@ -113,21 +115,21 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'resources', name: 'admin.resources', type: 'toggle', path: '/resources', icon: 'folder', pages: [ { - id: guid(), + id: 'resourcesWidgetsBundles', name: 'widget.widget-library', type: 'link', path: '/resources/widgets-bundles', icon: 'now_widgets' }, { - id: guid(), + id: 'resourcesResourcesLibrary', name: 'resource.resources-library', type: 'link', path: '/resources/resources-library', @@ -137,7 +139,7 @@ export class MenuService { ] }, { - id: guid(), + id: 'notification', name: 'notification.notification-center', type: 'link', path: '/notification', @@ -145,28 +147,28 @@ export class MenuService { isMdiIcon: true, pages: [ { - id: guid(), + id: 'notificationInbox', name: 'notification.inbox', type: 'link', path: '/notification/inbox', icon: 'inbox' }, { - id: guid(), + id: 'notificationSent', name: 'notification.sent', type: 'link', path: '/notification/sent', icon: 'outbox' }, { - id: guid(), + id: 'notificationRecipients', name: 'notification.recipients', type: 'link', path: '/notification/recipients', icon: 'contacts' }, { - id: guid(), + id: 'notificationTemplates', name: 'notification.templates', type: 'link', path: '/notification/templates', @@ -174,7 +176,7 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'notificationRules', name: 'notification.rules', type: 'link', path: '/notification/rules', @@ -184,28 +186,28 @@ export class MenuService { ] }, { - id: guid(), + id: 'settings', name: 'admin.settings', type: 'link', path: '/settings', icon: 'settings', pages: [ { - id: guid(), + id: 'settingsGeneral', name: 'admin.general', type: 'link', path: '/settings/general', icon: 'settings_applications' }, { - id: guid(), + id: 'settingsOutgoingMail', name: 'admin.outgoing-mail', type: 'link', path: '/settings/outgoing-mail', icon: 'mail' }, { - id: guid(), + id: 'settingsNotifications', name: 'admin.notifications', type: 'link', path: '/settings/notifications', @@ -213,7 +215,7 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'settingsQueues', name: 'admin.queues', type: 'link', path: '/settings/queues', @@ -222,21 +224,21 @@ export class MenuService { ] }, { - id: guid(), + id: 'securitySettings', name: 'security.security', type: 'toggle', path: '/security-settings', icon: 'security', pages: [ { - id: guid(), + id: 'securitySettingsGeneral', name: 'admin.general', type: 'link', path: '/security-settings/general', icon: 'settings_applications' }, { - id: guid(), + id: 'securitySettings2fa', name: 'admin.2fa.2fa', type: 'link', path: '/security-settings/2fa', @@ -244,7 +246,7 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'securitySettingsOauth2', name: 'admin.oauth2.oauth2', type: 'link', path: '/security-settings/oauth2', @@ -340,49 +342,49 @@ export class MenuService { const sections: Array = []; sections.push( { - id: guid(), + id: 'home', name: 'home.home', type: 'link', path: '/home', icon: 'home' }, { - id: guid(), + id: 'alarms', name: 'alarm.alarms', type: 'link', path: '/alarms', icon: 'notifications' }, { - id: guid(), + id: 'dashboards', name: 'dashboard.dashboards', type: 'link', path: '/dashboards', icon: 'dashboards' }, { - id: guid(), + id: 'entities', name: 'entity.entities', type: 'toggle', path: '/entities', icon: 'category', pages: [ { - id: guid(), + id: 'entitiesDevices', name: 'device.devices', type: 'link', path: '/entities/devices', icon: 'devices_other' }, { - id: guid(), + id: 'entitiesAssets', name: 'asset.assets', type: 'link', path: '/entities/assets', icon: 'domain' }, { - id: guid(), + id: 'entitiesEntityViews', name: 'entity-view.entity-views', type: 'link', path: '/entities/entityViews', @@ -391,14 +393,14 @@ export class MenuService { ] }, { - id: guid(), + id: 'profiles', name: 'profiles.profiles', type: 'toggle', path: '/profiles', icon: 'badge', pages: [ { - id: guid(), + id: 'profilesDeviceProfiles', name: 'device-profile.device-profiles', type: 'link', path: '/profiles/deviceProfiles', @@ -406,7 +408,7 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'profilesAssetProfiles', name: 'asset-profile.asset-profiles', type: 'link', path: '/profiles/assetProfiles', @@ -416,14 +418,14 @@ export class MenuService { ] }, { - id: guid(), + id: 'customers', name: 'customer.customers', type: 'link', path: '/customers', icon: 'supervisor_account' }, { - id: guid(), + id: 'ruleChains', name: 'rulechain.rulechains', type: 'link', path: '/ruleChains', @@ -433,21 +435,21 @@ export class MenuService { if (authState.edgesSupportEnabled) { sections.push( { - id: guid(), + id: 'edgeManagement', name: 'edge.management', type: 'toggle', path: '/edgeManagement', icon: 'settings_input_antenna', pages: [ { - id: guid(), + id: 'edgeManagementInstances', name: 'edge.instances', type: 'link', path: '/edgeManagement/instances', icon: 'router' }, { - id: guid(), + id: 'edgeManagementRuleChains', name: 'edge.rulechain-templates', type: 'link', path: '/edgeManagement/ruleChains', @@ -459,21 +461,21 @@ export class MenuService { } sections.push( { - id: guid(), + id: 'features', name: 'feature.advanced-features', type: 'toggle', path: '/features', icon: 'construction', pages: [ { - id: guid(), + id: 'featuresOtaUpdates', name: 'ota-update.ota-updates', type: 'link', path: '/features/otaUpdates', icon: 'memory' }, { - id: guid(), + id: 'featuresVc', name: 'version-control.version-control', type: 'link', path: '/features/vc', @@ -482,21 +484,21 @@ export class MenuService { ] }, { - id: guid(), + id: 'resources', name: 'admin.resources', type: 'toggle', path: '/resources', icon: 'folder', pages: [ { - id: guid(), + id: 'resourcesWidgetsBundles', name: 'widget.widget-library', type: 'link', path: '/resources/widgets-bundles', icon: 'now_widgets' }, { - id: guid(), + id: 'resourcesResourcesLibrary', name: 'resource.resources-library', type: 'link', path: '/resources/resources-library', @@ -506,7 +508,7 @@ export class MenuService { ] }, { - id: guid(), + id: 'notification', name: 'notification.notification-center', type: 'link', path: '/notification', @@ -514,28 +516,28 @@ export class MenuService { isMdiIcon: true, pages: [ { - id: guid(), + id: 'notificationInbox', name: 'notification.inbox', type: 'link', path: '/notification/inbox', icon: 'inbox' }, { - id: guid(), + id: 'notificationSent', name: 'notification.sent', type: 'link', path: '/notification/sent', icon: 'outbox' }, { - id: guid(), + id: 'notificationRecipients', name: 'notification.recipients', type: 'link', path: '/notification/recipients', icon: 'contacts' }, { - id: guid(), + id: 'notificationTemplates', name: 'notification.templates', type: 'link', path: '/notification/templates', @@ -543,7 +545,7 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'notificationRules', name: 'notification.rules', type: 'link', path: '/notification/rules', @@ -553,28 +555,28 @@ export class MenuService { ] }, { - id: guid(), + id: 'usage', name: 'api-usage.api-usage', type: 'link', path: '/usage', icon: 'insert_chart' }, { - id: guid(), + id: 'settings', name: 'admin.settings', type: 'link', path: '/settings', icon: 'settings', pages: [ { - id: guid(), + id: 'settingsHome', name: 'admin.home', type: 'link', path: '/settings/home', icon: 'settings_applications' }, { - id: guid(), + id: 'settingsNotifications', name: 'admin.notifications', type: 'link', path: '/settings/notifications', @@ -582,14 +584,14 @@ export class MenuService { isMdiIcon: true }, { - id: guid(), + id: 'settingsRepository', name: 'admin.repository', type: 'link', path: '/settings/repository', icon: 'manage_history' }, { - id: guid(), + id: 'settingsAutoCommit', name: 'admin.auto-commit', type: 'link', path: '/settings/auto-commit', @@ -598,14 +600,14 @@ export class MenuService { ] }, { - id: guid(), + id: 'securitySettings', name: 'security.security', type: 'toggle', path: '/security-settings', icon: 'security', pages: [ { - id: guid(), + id: 'securitySettingsAuditLogs', name: 'audit-log.audit-logs', type: 'link', path: '/security-settings/auditLogs', @@ -781,49 +783,49 @@ export class MenuService { const sections: Array = []; sections.push( { - id: guid(), + id: 'home', name: 'home.home', type: 'link', path: '/home', icon: 'home' }, { - id: guid(), + id: 'alarms', name: 'alarm.alarms', type: 'link', path: '/alarms', icon: 'notifications' }, { - id: guid(), + id: 'dashboards', name: 'dashboard.dashboards', type: 'link', path: '/dashboards', icon: 'dashboards' }, { - id: guid(), + id: 'entities', name: 'entity.entities', type: 'toggle', path: '/entities', icon: 'category', pages: [ { - id: guid(), + id: 'entitiesDevices', name: 'device.devices', type: 'link', path: '/entities/devices', icon: 'devices_other' }, { - id: guid(), + id: 'entitiesAssets', name: 'asset.assets', type: 'link', path: '/entities/assets', icon: 'domain' }, { - id: guid(), + id: 'entitiesEntityViews', name: 'entity-view.entity-views', type: 'link', path: '/entities/entityViews', @@ -835,7 +837,7 @@ export class MenuService { if (authState.edgesSupportEnabled) { sections.push( { - id: guid(), + id: 'edgeManagementInstances', name: 'edge.edge-instances', type: 'link', path: '/edgeManagement/instances', @@ -845,7 +847,7 @@ export class MenuService { } sections.push( { - id: guid(), + id: 'notification', name: 'notification.notification-center', type: 'link', path: '/notification', @@ -853,7 +855,7 @@ export class MenuService { isMdiIcon: true, pages: [ { - id: guid(), + id: 'notificationInbox', name: 'notification.inbox', type: 'link', path: '/notification/inbox', @@ -928,6 +930,19 @@ export class MenuService { return homeSections; } + private allMenuLinks(sections: Array): Array { + const result: Array = []; + for (const section of sections) { + if (section.type === 'link') { + result.push(section); + } + if (section.pages && section.pages.length) { + result.push(...this.allMenuLinks(section.pages)); + } + } + return result; + } + public menuSections(): Observable> { return this.menuSections$; } @@ -936,5 +951,20 @@ export class MenuService { return this.homeSections$; } -} + public availableMenuLinks(): Observable> { + return this.availableMenuLinks$; + } + public menuLinkById(id: string): Observable { + return this.availableMenuLinks$.pipe( + map((links) => links.find(link => link.id === id)) + ); + } + + public menuLinksByIds(ids: string[]): Observable> { + return this.availableMenuLinks$.pipe( + map((links) => links.filter(link => ids.includes(link.id))) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 53d60856dd..6e9e7f85fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -217,6 +217,7 @@ export interface TbFlotKeySettings { removeFromLegend: boolean; showLines: boolean; fillLines: boolean; + fillLinesOpacity: number; showPoints: boolean; showPointShape: string; pointShapeFormatter: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index 03e29769ff..08a8c92472 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -408,7 +408,7 @@ export class TbFlot { } } series.lines = { - fill: keySettings.fillLines === true + fill: keySettings.fillLines === true ? (keySettings.fillLinesOpacity || 0.4) : false }; if (this.settings.stack && !this.comparisonEnabled) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss index 427aa8bf3a..12b7bce17d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss @@ -17,23 +17,9 @@ @import "../../../../../../../scss/constants"; :host { - .tb-card-content { - width: 100%; - height: 100%; - } .tb-title { margin-top: 8px; - font-style: normal; - font-weight: 500; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.25px; - color: rgba(0, 0, 0, 0.54); - @media #{$mat-md-lg} { - font-size: 12px; - line-height: 16px; - } } .tb-info-icon { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts index be58092f1f..d9c2609dc7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts @@ -30,7 +30,7 @@ import { MediaBreakpoints } from '@shared/models/constants'; @Component({ selector: 'tb-configured-features', templateUrl: './configured-features.component.html', - styleUrls: ['./configured-features.component.scss'] + styleUrls: ['./home-page-widget.scss', './configured-features.component.scss'] }) export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html index 96c5642feb..1ccc2e8b24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html @@ -26,20 +26,20 @@ edit
- - - + + -
-
+ - -
+