From f079563daf486e0be4ef25998540dcb02f2be092 Mon Sep 17 00:00:00 2001 From: Philip Magyar Date: Thu, 22 Feb 2024 18:10:06 +0100 Subject: [PATCH 001/331] UI: sync country list & add flags to tb-contact country selector --- .../app/modules/home/models/contact.models.ts | 252 ------------------ .../shared/components/contact.component.html | 7 +- .../shared/components/contact.component.ts | 11 +- .../src/app/shared/models/country.models.ts | 15 ++ 4 files changed, 26 insertions(+), 259 deletions(-) diff --git a/ui-ngx/src/app/modules/home/models/contact.models.ts b/ui-ngx/src/app/modules/home/models/contact.models.ts index 41ebecc865..f1b23a0839 100644 --- a/ui-ngx/src/app/modules/home/models/contact.models.ts +++ b/ui-ngx/src/app/modules/home/models/contact.models.ts @@ -14,258 +14,6 @@ /// limitations under the License. /// -export const COUNTRIES = [ - 'Afghanistan', - 'ร…land Islands', - 'Albania', - 'Algeria', - 'American Samoa', - 'Andorra', - 'Angola', - 'Anguilla', - 'Antarctica', - 'Antigua and Barbuda', - 'Argentina', - 'Armenia', - 'Aruba', - 'Australia', - 'Austria', - 'Azerbaijan', - 'Bahamas', - 'Bahrain', - 'Bangladesh', - 'Barbados', - 'Belarus', - 'Belgium', - 'Belize', - 'Benin', - 'Bermuda', - 'Bhutan', - 'Bolivia', - 'Bonaire, Sint Eustatius and Saba', - 'Bosnia and Herzegovina', - 'Botswana', - 'Bouvet Island', - 'Brazil', - 'British Indian Ocean Territory', - 'Brunei Darussalam', - 'Bulgaria', - 'Burkina Faso', - 'Burundi', - 'Cambodia', - 'Cameroon', - 'Canada', - 'Cape Verde', - 'Cayman Islands', - 'Central African Republic', - 'Chad', - 'Chile', - 'China', - 'Christmas Island', - 'Cocos (Keeling) Islands', - 'Colombia', - 'Comoros', - 'Congo', - 'Congo, The Democratic Republic of the', - 'Cook Islands', - 'Costa Rica', - 'Cรดte d\'Ivoire', - 'Croatia', - 'Cuba', - 'Curaรงao', - 'Cyprus', - 'Czech Republic', - 'Denmark', - 'Djibouti', - 'Dominica', - 'Dominican Republic', - 'Ecuador', - 'Egypt', - 'El Salvador', - 'Equatorial Guinea', - 'Eritrea', - 'Estonia', - 'Ethiopia', - 'Falkland Islands (Malvinas)', - 'Faroe Islands', - 'Fiji', - 'Finland', - 'France', - 'French Guiana', - 'French Polynesia', - 'French Southern Territories', - 'Gabon', - 'Gambia', - 'Georgia', - 'Germany', - 'Ghana', - 'Gibraltar', - 'Greece', - 'Greenland', - 'Grenada', - 'Guadeloupe', - 'Guam', - 'Guatemala', - 'Guernsey', - 'Guinea', - 'Guinea-Bissau', - 'Guyana', - 'Haiti', - 'Heard Island and McDonald Islands', - 'Holy See (Vatican City State)', - 'Honduras', - 'Hong Kong', - 'Hungary', - 'Iceland', - 'India', - 'Indonesia', - 'Iran, Islamic Republic of', - 'Iraq', - 'Ireland', - 'Isle of Man', - 'Israel', - 'Italy', - 'Jamaica', - 'Japan', - 'Jersey', - 'Jordan', - 'Kazakhstan', - 'Kenya', - 'Kiribati', - 'Korea, Democratic People\'s Republic of', - 'Korea, Republic of', - 'Kuwait', - 'Kyrgyzstan', - 'Lao People\'s Democratic Republic', - 'Latvia', - 'Lebanon', - 'Lesotho', - 'Liberia', - 'Libya', - 'Liechtenstein', - 'Lithuania', - 'Luxembourg', - 'Macao', - 'Macedonia, Republic Of', - 'Madagascar', - 'Malawi', - 'Malaysia', - 'Maldives', - 'Mali', - 'Malta', - 'Marshall Islands', - 'Martinique', - 'Mauritania', - 'Mauritius', - 'Mayotte', - 'Mexico', - 'Micronesia, Federated States of', - 'Moldova, Republic of', - 'Monaco', - 'Mongolia', - 'Montenegro', - 'Montserrat', - 'Morocco', - 'Mozambique', - 'Myanmar', - 'Namibia', - 'Nauru', - 'Nepal', - 'Netherlands', - 'New Caledonia', - 'New Zealand', - 'Nicaragua', - 'Niger', - 'Nigeria', - 'Niue', - 'Norfolk Island', - 'Northern Mariana Islands', - 'Norway', - 'Oman', - 'Pakistan', - 'Palau', - 'Palestinian Territory, Occupied', - 'Panama', - 'Papua New Guinea', - 'Paraguay', - 'Peru', - 'Philippines', - 'Pitcairn', - 'Poland', - 'Portugal', - 'Puerto Rico', - 'Qatar', - 'Reunion', - 'Romania', - 'Russian Federation', - 'Rwanda', - 'Saint Barthรฉlemy', - 'Saint Helena, Ascension and Tristan da Cunha', - 'Saint Kitts and Nevis', - 'Saint Lucia', - 'Saint Martin (French Part)', - 'Saint Pierre and Miquelon', - 'Saint Vincent and the Grenadines', - 'Samoa', - 'San Marino', - 'Sao Tome and Principe', - 'Saudi Arabia', - 'Senegal', - 'Serbia', - 'Seychelles', - 'Sierra Leone', - 'Singapore', - 'Sint Maarten (Dutch Part)', - 'Slovakia', - 'Slovenia', - 'Solomon Islands', - 'Somalia', - 'South Africa', - 'South Georgia and the South Sandwich Islands', - 'South Sudan', - 'Spain', - 'Sri Lanka', - 'Sudan', - 'Suriname', - 'Svalbard and Jan Mayen', - 'Swaziland', - 'Sweden', - 'Switzerland', - 'Syrian Arab Republic', - 'Taiwan', - 'Tajikistan', - 'Tanzania, United Republic of', - 'Thailand', - 'Timor-Leste', - 'Togo', - 'Tokelau', - 'Tonga', - 'Trinidad and Tobago', - 'Tunisia', - 'Turkey', - 'Turkmenistan', - 'Turks and Caicos Islands', - 'Tuvalu', - 'Uganda', - 'Ukraine', - 'United Arab Emirates', - 'United Kingdom', - 'United States', - 'United States Minor Outlying Islands', - 'Uruguay', - 'Uzbekistan', - 'Vanuatu', - 'Venezuela', - 'Viet Nam', - 'Virgin Islands, British', - 'Virgin Islands, U.S.', - 'Wallis and Futuna', - 'Western Sahara', - 'Yemen', - 'Zambia', - 'Zimbabwe' -]; - /* eslint-disable */ export const POSTAL_CODE_PATTERNS = { 'United States': '(\\d{5}([\\-]\\d{4})?)', diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html index 059fd09c80..c0688be71a 100644 --- a/ui-ngx/src/app/shared/components/contact.component.html +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -18,9 +18,10 @@
contact.country - - - {{ country }} + + + {{country.flag}} + {{country.name }} diff --git a/ui-ngx/src/app/shared/components/contact.component.ts b/ui-ngx/src/app/shared/components/contact.component.ts index c5a3daecf8..dae5f2ac0d 100644 --- a/ui-ngx/src/app/shared/components/contact.component.ts +++ b/ui-ngx/src/app/shared/components/contact.component.ts @@ -16,11 +16,13 @@ import { Component, Input } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { COUNTRIES } from '@home/models/contact.models'; - +import { CountryData } from '../models/country.models'; @Component({ selector: 'tb-contact', - templateUrl: './contact.component.html' + templateUrl: './contact.component.html', + providers: [ + CountryData + ] }) export class ContactComponent { @@ -29,6 +31,7 @@ export class ContactComponent { @Input() isEdit: boolean; - countries = COUNTRIES; + countries = this.countryData.allCountries; + constructor(private countryData: CountryData) {} } diff --git a/ui-ngx/src/app/shared/models/country.models.ts b/ui-ngx/src/app/shared/models/country.models.ts index 814cb81147..919d362067 100644 --- a/ui-ngx/src/app/shared/models/country.models.ts +++ b/ui-ngx/src/app/shared/models/country.models.ts @@ -32,6 +32,7 @@ export enum CountryISO { Andorra = 'AD', Angola = 'AO', Anguilla = 'AI', + Antarctica = 'AQ', AntiguaAndBarbuda = 'AG', Argentina = 'AR', Armenia = 'AM', @@ -50,6 +51,8 @@ export enum CountryISO { Bermuda = 'BM', Bhutan = 'BT', Bolivia = 'BO', + BonaireAndSintEustatiusAndSaba = 'BQ', + BouvetIsland = 'BV', BosniaAndHerzegovina = 'BA', Botswana = 'BW', Brazil = 'BR', @@ -101,6 +104,7 @@ export enum CountryISO { France = 'FR', FrenchGuiana = 'GF', FrenchPolynesia = 'PF', + FrenchSouthernTerritories = 'TF', Gabon = 'GA', Gambia = 'GM', Georgia = 'GE', @@ -118,6 +122,7 @@ export enum CountryISO { GuineaBissau = 'GW', Guyana = 'GY', Haiti = 'HT', + HeardIslandandMcDonaldIslands = 'HM', Honduras = 'HN', HongKong = 'HK', Hungary = 'HU', @@ -195,6 +200,7 @@ export enum CountryISO { Paraguay = 'PY', Peru = 'PE', Philippines = 'PH', + PitcairnIslands = 'PN', Poland = 'PL', Portugal = 'PT', PuertoRico = 'PR', @@ -225,6 +231,7 @@ export enum CountryISO { SolomonIslands = 'SB', Somalia = 'SO', SouthAfrica = 'ZA', + SouthGeorgiaAndTheSouthSandwichIslands = 'GS', SouthKorea = 'KR', SouthSudan = 'SS', Spain = 'ES', @@ -280,6 +287,7 @@ export class CountryData { {name: 'Andorra', iso2: CountryISO.Andorra, dialCode: '376', flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ'}, {name: 'Angola', iso2: CountryISO.Angola, dialCode: '244', flag: '๐Ÿ‡ฆ๐Ÿ‡ด'}, {name: 'Anguilla', iso2: CountryISO.Anguilla, dialCode: '1', flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ'}, + {name: 'Antarctica', iso2: CountryISO.Antarctica, dialCode: '672', flag: '๐Ÿ‡ฆ๐Ÿ‡ถ'}, {name: 'Antigua and Barbuda', iso2: CountryISO.AntiguaAndBarbuda, dialCode: '1', flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ'}, {name: 'Argentina', iso2: CountryISO.Argentina, dialCode: '54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท'}, {name: 'Armenia', iso2: CountryISO.Armenia, dialCode: '374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ'}, @@ -298,6 +306,8 @@ export class CountryData { {name: 'Bermuda', iso2: CountryISO.Bermuda, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ฒ'}, {name: 'Bhutan', iso2: CountryISO.Bhutan, dialCode: '975', flag: '๐Ÿ‡ง๐Ÿ‡น'}, {name: 'Bolivia', iso2: CountryISO.Bolivia, dialCode: '591', flag: '๐Ÿ‡ง๐Ÿ‡ด'}, + {name: 'Bonaire, Sint Eustatius and Saba', iso2: CountryISO.BonaireAndSintEustatiusAndSaba, dialCode: '599', flag: '๐Ÿ‡ง๐Ÿ‡ถ'}, + {name: 'Bouvet Island', iso2: CountryISO.BouvetIsland, dialCode: '47', flag: '๐Ÿ‡ง๐Ÿ‡ป'}, {name: 'Bosnia and Herzegovina', iso2: CountryISO.BosniaAndHerzegovina, dialCode: '387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ'}, {name: 'Botswana', iso2: CountryISO.Botswana, dialCode: '267', flag: '๐Ÿ‡ง๐Ÿ‡ผ'}, {name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '๐Ÿ‡ง๐Ÿ‡ท'}, @@ -349,6 +359,7 @@ export class CountryData { {name: 'France', iso2: CountryISO.France, dialCode: '33', flag: '๐Ÿ‡ซ๐Ÿ‡ท'}, {name: 'French Guiana', iso2: CountryISO.FrenchGuiana, dialCode: '594', flag: '๐Ÿ‡ฌ๐Ÿ‡ซ'}, {name: 'French Polynesia', iso2: CountryISO.FrenchPolynesia, dialCode: '689', flag: '๐Ÿ‡ต๐Ÿ‡ซ'}, + {name: 'French Southern Territories', iso2: CountryISO.FrenchSouthernTerritories, dialCode: '262', flag: '๐Ÿ‡น๐Ÿ‡ซ'}, {name: 'Gabon', iso2: CountryISO.Gabon, dialCode: '241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ'}, {name: 'Gambia', iso2: CountryISO.Gambia, dialCode: '220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ'}, {name: 'Georgia', iso2: CountryISO.Georgia, dialCode: '995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช'}, @@ -366,6 +377,7 @@ export class CountryData { {name: 'Guinea-Bissau', iso2: CountryISO.GuineaBissau, dialCode: '245', flag: '๐Ÿ‡ฌ๐Ÿ‡ผ'}, {name: 'Guyana', iso2: CountryISO.Guyana, dialCode: '592', flag: '๐Ÿ‡ฌ๐Ÿ‡พ'}, {name: 'Haiti', iso2: CountryISO.Haiti, dialCode: '509', flag: '๐Ÿ‡ญ๐Ÿ‡น'}, + {name: 'Heard Island and McDonald Islands', iso2: CountryISO.HeardIslandandMcDonaldIslands, dialCode: '672', flag: '๐Ÿ‡ญ๐Ÿ‡ฒ'}, {name: 'Honduras', iso2: CountryISO.Honduras, dialCode: '504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ'}, {name: 'Hong Kong', iso2: CountryISO.HongKong, dialCode: '852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ'}, {name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '๐Ÿ‡ญ๐Ÿ‡บ'}, @@ -443,6 +455,7 @@ export class CountryData { {name: 'Paraguay', iso2: CountryISO.Paraguay, dialCode: '595', flag: '๐Ÿ‡ต๐Ÿ‡พ'}, {name: 'Peru', iso2: CountryISO.Peru, dialCode: '51', flag: '๐Ÿ‡ต๐Ÿ‡ช'}, {name: 'Philippines', iso2: CountryISO.Philippines, dialCode: '63', flag: '๐Ÿ‡ต๐Ÿ‡ญ'}, + {name: 'Pitcairn Islands', iso2: CountryISO.PitcairnIslands, dialCode: '649', flag: '๐Ÿ‡ต๐Ÿ‡ณ'}, {name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ'}, {name: 'Portugal', iso2: CountryISO.Portugal, dialCode: '351', flag: '๐Ÿ‡ต๐Ÿ‡น'}, {name: 'Puerto Rico', iso2: CountryISO.PuertoRico, dialCode: '1', flag: '๐Ÿ‡ต๐Ÿ‡ท'}, @@ -473,6 +486,8 @@ export class CountryData { {name: 'Solomon Islands', iso2: CountryISO.SolomonIslands, dialCode: '677', flag: '๐Ÿ‡ธ๐Ÿ‡ง'}, {name: 'Somalia', iso2: CountryISO.Somalia, dialCode: '252', flag: '๐Ÿ‡ธ๐Ÿ‡ด'}, {name: 'South Africa', iso2: CountryISO.SouthAfrica, dialCode: '27', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ'}, + {name: 'South Georgia and the South Sandwich Islands', + iso2: CountryISO.SouthGeorgiaAndTheSouthSandwichIslands, dialCode: '500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ'}, {name: 'South Korea', iso2: CountryISO.SouthKorea, dialCode: '82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท'}, {name: 'South Sudan', iso2: CountryISO.SouthSudan, dialCode: '211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ'}, {name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '๐Ÿ‡ช๐Ÿ‡ธ'}, From 4096a60612a7c3329830620764230e5fa27fb62a Mon Sep 17 00:00:00 2001 From: Philip Magyar Date: Thu, 29 Feb 2024 16:15:26 +0100 Subject: [PATCH 002/331] searchable countries --- .../shared/components/contact.component.html | 11 ++-- .../shared/components/contact.component.ts | 50 +++++++++++++++++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html index c0688be71a..6b997da07c 100644 --- a/ui-ngx/src/app/shared/components/contact.component.html +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -18,12 +18,13 @@
contact.country - - + + + {{country.flag}} {{country.name }} - +
@@ -56,7 +57,9 @@ contact.address2 - diff --git a/ui-ngx/src/app/shared/components/contact.component.ts b/ui-ngx/src/app/shared/components/contact.component.ts index dae5f2ac0d..410944f36a 100644 --- a/ui-ngx/src/app/shared/components/contact.component.ts +++ b/ui-ngx/src/app/shared/components/contact.component.ts @@ -14,9 +14,12 @@ /// limitations under the License. /// -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { CountryData } from '../models/country.models'; +import { Country, CountryData } from '../models/country.models'; +import { Observable, map, startWith } from 'rxjs'; +import { PhoneInputComponent } from './phone-input.component'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; @Component({ selector: 'tb-contact', templateUrl: './contact.component.html', @@ -24,14 +27,53 @@ import { CountryData } from '../models/country.models'; CountryData ] }) -export class ContactComponent { +export class ContactComponent implements OnInit { @Input() parentForm: UntypedFormGroup; @Input() isEdit: boolean; + @Input() phoneInputDefaultCountry = 'US'; + + @ViewChild('phoneInput') phoneInput: PhoneInputComponent | undefined; countries = this.countryData.allCountries; + countriesFiltered: Observable; + + constructor(private countryData: CountryData) { + this.displayCountryWithFlag = this.displayCountryWithFlag.bind(this); + } + + ngOnInit() { + this.countriesFiltered = this.parentForm.get('country').valueChanges + .pipe( + startWith(''), + map((countryName: string) => this._filterCountries(countryName)) + ); + } + + private _filterCountries(countryName: string): Country[] { + const filterValue = countryName.toLowerCase(); + return this.countries.filter(country => this.displayCountryWithFlag(country.name).toLowerCase().includes(filterValue)); + } + + displayCountryWithFlag(countryName: string): string { + const country = this.countries.find(c => c.name === countryName); + if (!country) { + return ''; + } + return country ? `${country.flag} ${country.name}` : ''; + } - constructor(private countryData: CountryData) {} + onCountryChange(select: MatAutocompleteSelectedEvent) { + // Get the selected country and check if the phone number is empty + // If it is, set the country code to the selected country + const countryName = select.option.value; + const country = this.countries.find(c => c.name === countryName); + if (country && this.phoneInput) { + if (!this.phoneInput.phoneFormGroup.get('phoneNumber').value) { + this.phoneInput.phoneFormGroup.get('country').setValue(country.iso2); + } + } + } } From eb50048c5001b61f81c400cf6af97fcff9b16223 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 2 May 2024 19:44:47 +0300 Subject: [PATCH 003/331] UI: Scada widget test --- .../basic/basic-widget-config.module.ts | 12 +- .../scada-test-basic-config.component.html | 26 ++ .../scada-test-basic-config.component.ts | 64 +++ .../scada/scada-test-widget.component.html | 19 + .../scada/scada-test-widget.component.scss | 22 + .../lib/scada/scada-test-widget.component.ts | 102 +++++ .../lib/scada/scada-test-widget.models.ts | 21 + .../widget/lib/scada/scada.models.ts | 398 ++++++++++++++++++ .../scada-object-settings.component.html | 48 +++ .../scada/scada-object-settings.component.ts | 168 ++++++++ .../common/widget-settings-common.module.ts | 5 + .../widget/widget-components.module.ts | 7 +- ui-ngx/src/assets/widget/scada/drawing.svg | 149 +++++++ 13 files changed, 1036 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts create mode 100644 ui-ngx/src/assets/widget/scada/drawing.svg diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 18b80f86a5..79af9bee18 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -129,6 +129,9 @@ import { import { RadarChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/radar-chart-basic-config.component'; +import { + ScadaTestBasicConfigComponent +} from '@home/components/widget/config/basic/scada/scada-test-basic-config.component'; @NgModule({ declarations: [ @@ -171,7 +174,8 @@ import { PieChartBasicConfigComponent, BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, - RadarChartBasicConfigComponent + RadarChartBasicConfigComponent, + ScadaTestBasicConfigComponent ], imports: [ CommonModule, @@ -216,7 +220,8 @@ import { PieChartBasicConfigComponent, BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, - RadarChartBasicConfigComponent + RadarChartBasicConfigComponent, + ScadaTestBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -255,5 +260,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts new file mode 100644 index 0000000000..3d6a4a6a45 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts @@ -0,0 +1,64 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TargetDevice, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models'; + +@Component({ + selector: 'tb-scada-test-basic-config', + templateUrl: './scada-test-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.scadaTestWidgetConfigForm.get('targetDevice').value; + } + + scadaTestWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.scadaTestWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: ScadaTestWidgetSettings = {...(configData.config.settings as ScadaTestWidgetSettings || { scadaObject: {}})}; + this.scadaTestWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + scadaObject: [settings.scadaObject, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.scadaObject = config.scadaObject; + return this.widgetConfig; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html new file mode 100644 index 0000000000..f811e7f889 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html @@ -0,0 +1,19 @@ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss new file mode 100644 index 0000000000..5012fe3a7c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts new file mode 100644 index 0000000000..25451dcf15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts @@ -0,0 +1,102 @@ +/// +/// Copyright ยฉ 2016-2024 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 { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + OnInit, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { SVG, Svg } from '@svgdotjs/svg.js'; +import { HttpClient } from '@angular/common/http'; +import { ScadaObject, ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models'; + +@Component({ + selector: 'tb-scada-test-widget', + templateUrl: './scada-test-widget.component.html', + styleUrls: ['../action/action-widget.scss', './scada-test-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaTestWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('scadaShape', {static: false}) + scadaShape: ElementRef; + + private settings: ScadaTestWidgetSettings; + + private autoScale = true; + private scadaObject: ScadaObject; + + private shapeResize$: ResizeObserver; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + protected cd: ChangeDetectorRef, + private http: HttpClient) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...this.ctx.settings}; + this.scadaObject = new ScadaObject(this.ctx, '/assets/widget/scada/drawing.svg', this.settings.scadaObject); + this.scadaObject.init().subscribe(); + } + + ngAfterViewInit(): void { + this.scadaObject.addTo(this.scadaShape.nativeElement); + if (this.autoScale) { + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.scadaShape.nativeElement); + this.onResize(); + } + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + if (this.scadaObject) { + this.scadaObject.destroy(); + } + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + } + + private onResize() { + const shapeWidth = this.scadaShape.nativeElement.getBoundingClientRect().width; + const shapeHeight = this.scadaShape.nativeElement.getBoundingClientRect().height; + this.scadaObject.setSize(shapeWidth, shapeHeight); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts new file mode 100644 index 0000000000..a74d81e3d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts @@ -0,0 +1,21 @@ +/// +/// Copyright ยฉ 2016-2024 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 { ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models'; + +export interface ScadaTestWidgetSettings { + scadaObject: ScadaObjectSettings; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts new file mode 100644 index 0000000000..7088fe6131 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts @@ -0,0 +1,398 @@ +/// +/// Copyright ยฉ 2016-2024 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 { ValueType } from '@shared/models/constants'; +import { Box, Element, Runner, Svg, SVG, Timeline } from '@svgdotjs/svg.js'; +import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; +import { insertVariable, isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep } from '@core/utils'; +import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; +import { map, share } from 'rxjs/operators'; +import { ValueAction, ValueGetter } from '@home/components/widget/lib/action/action-widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; + +export type ValueMatcherType = 'any' | 'constant' | 'range'; + +export interface ValueMatcher { + type: ValueMatcherType; + value?: any; + range?: {from: number; to: number}; +} + +export type ScadaObjectAttributeValueType = 'input' | 'property'; + +export interface ScadaObjectAttributeValue { + type: ScadaObjectAttributeValueType; + propertyId?: string; +} + +export interface ScadaObjectAttributeState { + name: string; + value: ScadaObjectAttributeValue; +} + +export interface ScadaObjectState { + tag: string; + attributes: ScadaObjectAttributeState[]; + animate?: number; +} + +export interface ScadaObjectUpdateState { + matcher: ValueMatcher; + state: ScadaObjectState[]; +} + +export enum ScadaObjectBehaviorType { + setValue = 'setValue', + getValue = 'getValue' +} + +export interface ScadaObjectBehaviorBase { + id: string; + name: string; + type: ScadaObjectBehaviorType; +} + +export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase { + valueType: ValueType; + defaultValue: any; + onUpdate: ScadaObjectUpdateState[]; +} + +export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase { + todo: any; +} + +export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet; + +export type ScadaObjectPropertyType = 'string' | 'number' | 'color'; + +export interface ScadaObjectPropertyBase { + id: string; + name: string; + type: ScadaObjectPropertyType; + default: any; +} + +export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase { + min?: number; + max?: number; +} + +export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty; + +export interface ScadaObjectMetadata { + title: string; + initial: ScadaObjectState[]; + behavior: ScadaObjectBehavior[]; + properties: ScadaObjectProperty[]; +} + +export const emptyMetadata: ScadaObjectMetadata = { + title: '', + initial: [], + behavior: [], + properties: [] +}; + + +export const parseScadaObjectMetadataFromContent = (svgContent: string): ScadaObjectMetadata => { + try { + const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + return parseScadaObjectMetadataFromDom(svgDoc); + } catch (_e) { + return emptyMetadata; + } +}; + +const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata => { + try { + const elements = svgDoc.getElementsByTagName('tb:metadata'); + if (elements.length) { + return JSON.parse(elements[0].innerHTML); + } else { + return emptyMetadata; + } + } catch (_e) { + return emptyMetadata; + } +}; + +const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings => ({ + action: GetValueAction.DO_NOTHING, + defaultValue: get.defaultValue, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }); + +export const defaultScadaObjectSettings = (metadata: ScadaObjectMetadata): ScadaObjectSettings => { + const settings: ScadaObjectSettings = {}; + for (const behaviour of metadata.behavior) { + //behaviour.id + if (behaviour.type === ScadaObjectBehaviorType.getValue) { + settings[behaviour.id] = defaultGetValueSettings(behaviour as ScadaObjectBehaviorGet); + } else if (behaviour.type === ScadaObjectBehaviorType.setValue) { + // TODO: + } + } + for (const property of metadata.properties) { + settings[property.id] = property.default; + } + return settings; +}; + +export type ScadaObjectSettings = {[id: string]: any}; + +export class ScadaObject { + + private metadata: ScadaObjectMetadata; + private settings: ScadaObjectSettings; + + private rootElement: HTMLElement; + private svgShape: Svg; + private box: Box; + private targetWidth: number; + private targetHeight: number; + + private loadingSubject = new BehaviorSubject(false); + private valueGetters: ValueGetter[] = []; + private valueActions: ValueAction[] = []; + + private animationTimeline: Timeline; + + loading$ = this.loadingSubject.asObservable().pipe(share()); + + constructor(private ctx: WidgetContext, + private svgPath: string, + private inputSettings: ScadaObjectSettings) {} + + public init(): Observable { + return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( + map((inputSvgContent) => { + const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); + this.metadata = parseScadaObjectMetadataFromDom(doc); + const defaults = defaultScadaObjectSettings(this.metadata); + this.settings = mergeDeep({}, defaults, this.inputSettings || {}); + this.prepareSvgShape(doc); + this.prepareStates(); + }) + ); + } + + public addTo(element: HTMLElement) { + this.rootElement = element; + if (this.svgShape) { + this.svgShape.addTo(element); + } + } + + public destroy() { + this.valueActions.forEach(v => v.destroy()); + this.loadingSubject.complete(); + this.loadingSubject.unsubscribe(); + } + + public setSize(targetWidth: number, targetHeight: number) { + this.targetWidth = targetWidth; + this.targetHeight = targetHeight; + if (this.svgShape) { + this.resize(); + } + } + + private prepareSvgShape(doc: XMLDocument) { + const elements = doc.getElementsByTagName('tb:metadata'); + for (let i=0;i = this.settings[getBehavior.id]; + getValueSettings = {...getValueSettings, actionLabel: getBehavior.name}; + const valueGetter = + ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, { + next: (val) => {this.onValue(getBehavior.id, val);}, + error: (e) => {} + }); + this.valueGetters.push(valueGetter); + this.valueActions.push(valueGetter); + } + } + if (this.metadata.initial) { + this.updateState(this.metadata.initial); + } + if (this.valueGetters.length) { + const getValueObservables: Array> = []; + this.valueGetters.forEach(valueGetter => { + getValueObservables.push(valueGetter.getValue()); + }); + this.loadingSubject.next(true); + forkJoin(getValueObservables).subscribe( + { + next: () => { + this.loadingSubject.next(false); + }, + error: () => { + this.loadingSubject.next(false); + } + } + ); + } + } + + private resize() { + let scale: number; + if (this.targetWidth < this.targetHeight) { + scale = this.targetWidth / this.box.width; + } else { + scale = this.targetHeight / this.box.height; + } + this.svgShape.node.style.transform = `scale(${scale})`; + } + + private onValue(id: string, value: any) { + const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet; + value = this.normalizeValue(value, getBehavior.valueType); + const updateStates = this.filterUpdateStates(getBehavior.onUpdate, value); + if (this.animationTimeline) { + this.animationTimeline.finish(); + } + for (const updateState of updateStates) { + this.updateState(updateState.state, value); + } + } + + private updateState(state: ScadaObjectState[], value?: any) { + for (const stateEntry of state) { + const tag = stateEntry.tag; + const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); + const attrs = this.computeAttributes(stateEntry.attributes, value); + elements.forEach(e => { + this.setElementAttributes(e, attrs, stateEntry.animate); + }); + } + } + + private normalizeValue(value: any, type: ValueType): any { + if (isUndefinedOrNull(value)) { + switch (type) { + case ValueType.STRING: + return ''; + case ValueType.INTEGER: + case ValueType.DOUBLE: + return 0; + case ValueType.BOOLEAN: + return false; + case ValueType.JSON: + return {}; + } + } else { + return value; + } + } + + private computeAttributes(attributes: ScadaObjectAttributeState[], value: any): {[attr: string]: any} { + const res: {[attr: string]: any} = {}; + for (const attribute of attributes) { + const attr = attribute.name; + res[attr] = this.getAttributeValue(attribute, value); + } + return res; + } + + private setElementAttributes(element: Element, attrs: {[attr: string]: any}, animate?: number) { + if (isDefinedAndNotNull(animate)) { + this.animation(element, animate).attr(attrs); + } else { + element.attr(attrs); + } + } + + private animation(element: Element, duration: number): Runner { + if (!this.animationTimeline) { + this.animationTimeline = new Timeline(); + } + element.timeline(this.animationTimeline); + return element.animate(duration, 0, 'now'); + } + + private getAttributeValue(attribute: ScadaObjectAttributeState, value: any): any { + if (attribute.value.type === 'input') { + return value; + } else if (attribute.value.type === 'property') { + const id = attribute.value.propertyId; + return this.settings[id] || ''; + } else { + return ''; + } + } + + private filterUpdateStates(states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] { + return states.filter(s => this.valueMatches(s.matcher, val)); + } + + private valueMatches(matcher: ValueMatcher, val: any): boolean { + switch (matcher.type) { + case 'any': + return true; + case 'constant': + return matcher.value === val; + case 'range': + if (isDefinedAndNotNull(val) && isNumeric(val)) { + const num = Number(val); + const range = matcher.range; + return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)); + } else { + return false; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html new file mode 100644 index 0000000000..d5a8cba6df --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html @@ -0,0 +1,48 @@ + + +
{{ metadata?.title }}
+
+
widgets.slider.behavior
+
+
{{ behaviour.name }}
+ + +
+
+
+
+
{{ property.name }}
+ + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts new file mode 100644 index 0000000000..ad10d681a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts @@ -0,0 +1,168 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + defaultScadaObjectSettings, + parseScadaObjectMetadataFromContent, + ScadaObjectBehaviorType, + ScadaObjectMetadata, + ScadaObjectSettings +} from '@home/components/widget/lib/scada/scada.models'; +import { HttpClient } from '@angular/common/http'; +import { ValueType } from '@shared/models/constants'; +import { IAliasController } from '@core/api/widget-api.models'; +import { TargetDevice, widgetType } from '@shared/models/widget.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-scada-object-settings', + templateUrl: './scada-object-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaObjectSettingsComponent), + multi: true + } + ] +}) +export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor { + + ScadaObjectBehaviorType = ScadaObjectBehaviorType; + + @Input() + disabled: boolean; + + @Input() + svgPath = '/assets/widget/scada/drawing.svg'; + + @Input() + aliasController: IAliasController; + + @Input() + targetDevice: TargetDevice; + + @Input() + widgetType: widgetType; + + private modelValue: ScadaObjectSettings; + + private propagateChange = null; + + public scadaObjectSettingsFormGroup: UntypedFormGroup; + + metadata: ScadaObjectMetadata; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private http: HttpClient) { + } + + ngOnInit(): void { + this.scadaObjectSettingsFormGroup = this.fb.group({}); + this.scadaObjectSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.loadMetadata(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['svgPath'].includes(propName)) { + this.loadMetadata(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.scadaObjectSettingsFormGroup.disable({emitEvent: false}); + } else { + this.scadaObjectSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaObjectSettings): void { + this.modelValue = value || {}; + this.setupValue(); + } + + private loadMetadata() { + this.http.get(this.svgPath, {responseType: 'text'}).subscribe( + (svgContent) => { + this.metadata = parseScadaObjectMetadataFromContent(svgContent); + for (const control of Object.keys(this.scadaObjectSettingsFormGroup.controls)) { + this.scadaObjectSettingsFormGroup.removeControl(control, {emitEvent: false}); + } + for (const behaviour of this.metadata.behavior) { + this.scadaObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false}); + } + for (const property of this.metadata.properties) { + const validators: ValidatorFn[] = []; + if (property.type === 'number') { + if (isDefinedAndNotNull(property.min)) { + validators.push(Validators.min(property.min)); + } + if (isDefinedAndNotNull(property.max)) { + validators.push(Validators.max(property.max)); + } + } + this.scadaObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); + } + this.setupValue(); + } + ); + } + + private setupValue() { + if (this.metadata) { + const defaults = defaultScadaObjectSettings(this.metadata); + this.modelValue = {...defaults, ...this.modelValue}; + this.scadaObjectSettingsFormGroup.patchValue( + this.modelValue, {emitEvent: false} + ); + } + } + + private updateModel() { + this.modelValue = this.scadaObjectSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } + + protected readonly ValueType = ValueType; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index bbe3dfb20c..152cf25b35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -149,6 +149,9 @@ import { StatusWidgetStateSettingsComponent } from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component'; +import { + ScadaObjectSettingsComponent +} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.component'; @NgModule({ declarations: [ @@ -204,6 +207,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/ TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, + ScadaObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -265,6 +269,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/ TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, + ScadaObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 69185b6dd2..f062494763 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -92,6 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component'; import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component'; import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component'; +import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scada-test-widget.component'; @NgModule({ declarations: @@ -150,7 +151,8 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad PieChartWidgetComponent, BarChartWidgetComponent, PolarAreaWidgetComponent, - RadarChartWidgetComponent + RadarChartWidgetComponent, + ScadaTestWidgetComponent ], imports: [ CommonModule, @@ -212,7 +214,8 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad PieChartWidgetComponent, BarChartWidgetComponent, PolarAreaWidgetComponent, - RadarChartWidgetComponent + RadarChartWidgetComponent, + ScadaTestWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/assets/widget/scada/drawing.svg b/ui-ngx/src/assets/widget/scada/drawing.svg new file mode 100644 index 0000000000..972ca550cd --- /dev/null +++ b/ui-ngx/src/assets/widget/scada/drawing.svg @@ -0,0 +1,149 @@ + + + { + "title": "My first SCADA Object", + "initial": [ + { + "tag": "RECT", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "background" + } + }, + { + "name": "stroke", + "value": { + "type": "property", + "propertyId": "strokeColor" + } + }, + { + "name": "stroke-width", + "value": { + "type": "property", + "propertyId": "strokeWidth" + } + } + ] + } + ], + "behavior": [ + { + "id": "initialState", + "name": "Initial state", + "type": "getValue", + "valueType": "DOUBLE", + "defaultValue": 0, + "onUpdate": [ + { + "matcher": { + "type": "any" + }, + "state": [ + { + "tag": "RECT", + "animate": 200, + "attributes": [ + { + "name": "height", + "value": { + "type": "input" + } + }, + { + "name": "width", + "value": { + "type": "input" + } + } + ] + } + ] + } + ] + }, + { + "id": "disabledState", + "name": "Disabled state", + "type": "getValue", + "valueType": "BOOLEAN", + "defaultValue": false, + "onUpdate": [ + { + "matcher": { + "type": "constant", + "value": true + }, + "state": [ + { + "tag": "RECT", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "disabledBackground" + } + } + ] + } + ] + }, + { + "matcher": { + "type": "constant", + "value": false + }, + "state": [ + { + "tag": "RECT", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "background" + } + } + ] + } + ] + } + ] + } + ], + "properties": [ + { + "id": "strokeColor", + "name": "Stroke color", + "type": "color", + "default": "#aaa" + }, + { + "id": "strokeWidth", + "name": "Stroke width", + "type": "number", + "default": 5, + "min": 0, + "max": 20 + }, + { + "id": "background", + "name": "Background", + "type": "color", + "default": "green" + }, + { + "id": "disabledBackground", + "name": "Disabled background", + "type": "color", + "default": "#ccc" + } + ] + } + + + From f01f99e738c8e2d3b2e21dd5cb8770949ee0dc45 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 3 May 2024 19:21:07 +0300 Subject: [PATCH 004/331] UI: Scada metadata and configuration improvements. --- .../widget/lib/scada/scada.models.ts | 325 +++++++++++++--- .../scada-object-settings.component.html | 47 ++- .../scada/scada-object-settings.component.ts | 75 +++- .../scada/scada-object-settings.models.ts | 43 +++ ui-ngx/src/assets/widget/scada/drawing.svg | 352 +++++++++++++----- 5 files changed, 673 insertions(+), 169 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts index 7088fe6131..542e24844e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts @@ -15,13 +15,23 @@ /// import { ValueType } from '@shared/models/constants'; -import { Box, Element, Runner, Svg, SVG, Timeline } from '@svgdotjs/svg.js'; +import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js'; import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; -import { insertVariable, isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep } from '@core/utils'; +import { + formatValue, + insertVariable, + isDefinedAndNotNull, + isNumber, + isNumeric, + isUndefinedOrNull, + mergeDeep, + parseFunction +} from '@core/utils'; import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; import { map, share } from 'rxjs/operators'; import { ValueAction, ValueGetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; +import { Font } from '@shared/models/widget-settings.models'; export type ValueMatcherType = 'any' | 'constant' | 'range'; @@ -31,29 +41,85 @@ export interface ValueMatcher { range?: {from: number; to: number}; } -export type ScadaObjectAttributeValueType = 'input' | 'property'; +const valueMatches = (matcher: ValueMatcher, val: any): boolean => { + switch (matcher.type) { + case 'any': + return true; + case 'constant': + return matcher.value === val; + case 'range': + if (isDefinedAndNotNull(val) && isNumeric(val)) { + const num = Number(val); + const range = matcher.range; + return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)); + } else { + return false; + } + } +}; -export interface ScadaObjectAttributeValue { - type: ScadaObjectAttributeValueType; +export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat'; + +export interface ScadaObjectValueBase { + type: ScadaObjectValueType; +} + +export interface ScadaObjectValueConstant extends ScadaObjectValueBase { + constantValue?: any; +} + +export interface ScadaObjectValueProperty extends ScadaObjectValueBase { propertyId?: string; + computedPropertyValue?: any; } -export interface ScadaObjectAttributeState { +export interface ScadaObjectValueFunction extends ScadaObjectValueBase { + valueConvertFunction?: string; + valueConverter?: (val: any) => any; +} + +export interface ScadaObjectValueFormat extends ScadaObjectValueBase { + units?: ScadaObjectValue; + decimals?: ScadaObjectValue; + computedUnits?: string; + computedDecimals?: number; +} + +export type ScadaObjectValue = ScadaObjectValueProperty & ScadaObjectValueConstant & ScadaObjectValueFunction & ScadaObjectValueFormat; + +export interface ScadaObjectAttribute { name: string; - value: ScadaObjectAttributeValue; + value: ScadaObjectValue; } -export interface ScadaObjectState { +export interface ScadaObjectText { + content?: ScadaObjectValue; + font?: ScadaObjectValue; + color?: ScadaObjectValue; +} + +export interface ScadaObjectElementState { tag: string; - attributes: ScadaObjectAttributeState[]; + show?: ScadaObjectValue; + text?: ScadaObjectText; + attributes?: ScadaObjectAttribute[]; animate?: number; } +export interface ScadaObjectState { + initial?: boolean; + value?: any; + state: ScadaObjectElementState[]; +} + export interface ScadaObjectUpdateState { matcher: ValueMatcher; - state: ScadaObjectState[]; + stateId: string; } +const filterUpdateStates = (states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] => + states.filter(s => valueMatches(s.matcher, val)); + export enum ScadaObjectBehaviorType { setValue = 'setValue', getValue = 'getValue' @@ -77,32 +143,38 @@ export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase { export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet; -export type ScadaObjectPropertyType = 'string' | 'number' | 'color'; +export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'font' | 'units' | 'switch'; export interface ScadaObjectPropertyBase { id: string; name: string; type: ScadaObjectPropertyType; default: any; + required?: boolean; + subLabel?: string; + divider?: boolean; + fieldSuffix?: string; + disableOnProperty?: string; } export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase { min?: number; max?: number; + step?: number; } export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty; export interface ScadaObjectMetadata { title: string; - initial: ScadaObjectState[]; + states: {[id: string]: ScadaObjectState}; behavior: ScadaObjectBehavior[]; properties: ScadaObjectProperty[]; } export const emptyMetadata: ScadaObjectMetadata = { title: '', - initial: [], + states: {}, behavior: [], properties: [] }; @@ -126,6 +198,7 @@ const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata return emptyMetadata; } } catch (_e) { + console.error(_e); return emptyMetadata; } }; @@ -201,8 +274,9 @@ export class ScadaObject { this.metadata = parseScadaObjectMetadataFromDom(doc); const defaults = defaultScadaObjectSettings(this.metadata); this.settings = mergeDeep({}, defaults, this.inputSettings || {}); + this.prepareMetadata(); this.prepareSvgShape(doc); - this.prepareStates(); + this.initStates(); }) ); } @@ -228,17 +302,58 @@ export class ScadaObject { } } + private prepareMetadata() { + for (const stateId of Object.keys(this.metadata.states)) { + const state = this.metadata.states[stateId]; + for (const elementState of state.state) { + this.prepareValue(elementState.show); + this.prepareValue(elementState.text?.content); + this.prepareValue(elementState.text?.font); + this.prepareValue(elementState.text?.color); + if (elementState.attributes) { + for (const attribute of elementState.attributes) { + this.prepareValue(attribute.value); + } + } + } + } + } + + private prepareValue(value: ScadaObjectValue) { + if (value) { + if (value.type === 'function' && value.valueConvertFunction) { + try { + value.valueConverter = parseFunction(this.insertVariables(value.valueConvertFunction), ['value']); + } catch (e) { + value.valueConverter = (v) => v; + } + } else if (value.type === 'property') { + value.computedPropertyValue = this.getPropertyValue(value.propertyId); + } else if (value.type === 'valueFormat') { + if (value.units) { + this.prepareValue(value.units); + } + if (value.decimals) { + this.prepareValue(value.decimals); + } + } + } + } + + private insertVariables(content: string): string { + for (const property of this.metadata.properties) { + const value = this.getPropertyValue(property.id); + content = insertVariable(content, property.id, value); + } + return content; + } + private prepareSvgShape(doc: XMLDocument) { const elements = doc.getElementsByTagName('tb:metadata'); for (let i=0;i s.initial); + if (initialState) { + this.updateState(initialState); } if (this.valueGetters.length) { const getValueObservables: Array> = []; @@ -301,23 +417,61 @@ export class ScadaObject { private onValue(id: string, value: any) { const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet; value = this.normalizeValue(value, getBehavior.valueType); - const updateStates = this.filterUpdateStates(getBehavior.onUpdate, value); + const updateStates = filterUpdateStates(getBehavior.onUpdate, value); if (this.animationTimeline) { this.animationTimeline.finish(); } for (const updateState of updateStates) { - this.updateState(updateState.state, value); + const state = this.metadata.states[updateState.stateId]; + this.updateState(state, value); } } - private updateState(state: ScadaObjectState[], value?: any) { - for (const stateEntry of state) { - const tag = stateEntry.tag; - const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); - const attrs = this.computeAttributes(stateEntry.attributes, value); - elements.forEach(e => { - this.setElementAttributes(e, attrs, stateEntry.animate); - }); + private updateState(state: ScadaObjectState, value?: any) { + if (state) { + for (const elementState of state.state) { + const tag = elementState.tag; + const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); + if (elements.length) { + if (elementState.show) { + const show: boolean = this.computeValue(elementState.show, value); + elements.forEach(e => { + if (show) { + e.show(); + } else { + e.hide(); + } + }); + } + if (elementState.attributes) { + const attrs = this.computeAttributes(elementState.attributes, value); + elements.forEach(e => { + this.setElementAttributes(e, attrs, elementState.animate); + }); + } + if (elementState.text) { + if (elementState.text.content) { + const text: string = this.computeValue(elementState.text.content, value); + elements.forEach(e => { + this.setElementText(e, text); + }); + } + if (elementState.text.font || elementState.text.color) { + let font: Font = this.computeValue(elementState.text.font, value); + if (typeof font !== 'object') { + font = undefined; + } + let color: string = this.computeValue(elementState.text.color, value); + if (typeof color !== 'string') { + color = undefined; + } + elements.forEach(e => { + this.setElementFont(e, font, color); + }); + } + } + } + } } } @@ -339,11 +493,11 @@ export class ScadaObject { } } - private computeAttributes(attributes: ScadaObjectAttributeState[], value: any): {[attr: string]: any} { + private computeAttributes(attributes: ScadaObjectAttribute[], value: any): {[attr: string]: any} { const res: {[attr: string]: any} = {}; for (const attribute of attributes) { const attr = attribute.name; - res[attr] = this.getAttributeValue(attribute, value); + res[attr] = this.computeValue(attribute.value, value); } return res; } @@ -356,6 +510,41 @@ export class ScadaObject { } } + private setElementText(element: Element, text: string) { + let textElement: Text; + if (element.type === 'text') { + const children = element.children(); + if (children.length && children[0].type === 'tspan') { + textElement = children[0] as Text; + } else { + textElement = element as Text; + } + } else if (element.type === 'tspan') { + textElement = element as Text; + } + if (textElement) { + textElement.text(text); + } + } + + private setElementFont(element: Element, font: Font, color: string) { + if (element.type === 'text') { + const textElement = element as Text; + if (font) { + textElement.font({ + family: font.family, + size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ? + font.size + font.sizeUnit : null, + weight: font.weight, + style: font.style + }); + } + if (color) { + textElement.fill(color); + } + } + } + private animation(element: Element, duration: number): Runner { if (!this.animationTimeline) { this.animationTimeline = new Timeline(); @@ -364,35 +553,55 @@ export class ScadaObject { return element.animate(duration, 0, 'now'); } - private getAttributeValue(attribute: ScadaObjectAttributeState, value: any): any { - if (attribute.value.type === 'input') { - return value; - } else if (attribute.value.type === 'property') { - const id = attribute.value.propertyId; - return this.settings[id] || ''; + private computeValue(objectValue: ScadaObjectValue, value: any): any { + if (objectValue) { + switch (objectValue.type) { + case 'input': + return value; + case 'constant': + return objectValue.constantValue; + case 'property': + return objectValue.computedPropertyValue; + case 'function': + try { + return objectValue.valueConverter(value); + } catch (_e) { + return value; + } + case 'valueFormat': + let units = ''; + let decimals = 0; + if (objectValue.units) { + units = this.computeValue(objectValue.units, value); + } + if (objectValue.decimals) { + decimals = this.computeValue(objectValue.decimals, value); + } + return formatValue(value, decimals, units, false); + } } else { return ''; } } - private filterUpdateStates(states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] { - return states.filter(s => this.valueMatches(s.matcher, val)); - } - - private valueMatches(matcher: ValueMatcher, val: any): boolean { - switch (matcher.type) { - case 'any': - return true; - case 'constant': - return matcher.value === val; - case 'range': - if (isDefinedAndNotNull(val) && isNumeric(val)) { - const num = Number(val); - const range = matcher.range; - return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)); - } else { - return false; + private getPropertyValue(id: string): any { + const property = this.metadata.properties.find(p => p.id === id); + if (property) { + const value = this.settings[id]; + if (isDefinedAndNotNull(value)) { + return value; + } else { + switch (property.type) { + case 'string': + return ''; + case 'number': + return 0; + case 'color': + return '#000'; } + } + } else { + return ''; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html index d5a8cba6df..2907a951dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html @@ -32,17 +32,42 @@
-
-
-
{{ property.name }}
- - - - - +
+
+ + {{ propertyRow.label }} + +
{{ propertyRow.label }}
+
+ +
{{ property.subLabel }}
+ + +
{{ property.fieldSuffix }}
+
+ + + + +
{{ property.fieldSuffix }}
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts index ad10d681a7..3e0bd7409d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts @@ -14,12 +14,15 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, + UntypedFormControl, UntypedFormGroup, + Validator, ValidatorFn, Validators } from '@angular/forms'; @@ -37,6 +40,11 @@ import { ValueType } from '@shared/models/constants'; import { IAliasController } from '@core/api/widget-api.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { isDefinedAndNotNull } from '@core/utils'; +import { + ScadaPropertyRow, + toPropertyRows +} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.models'; +import { merge, Observable, Subscription } from 'rxjs'; @Component({ selector: 'tb-scada-object-settings', @@ -47,10 +55,15 @@ import { isDefinedAndNotNull } from '@core/utils'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ScadaObjectSettingsComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaObjectSettingsComponent), + multi: true } ] }) -export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor { +export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { ScadaObjectBehaviorType = ScadaObjectBehaviorType; @@ -73,13 +86,18 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV private propagateChange = null; + private validatorTriggers: string[]; + private validatorSubscription: Subscription; + public scadaObjectSettingsFormGroup: UntypedFormGroup; metadata: ScadaObjectMetadata; + propertyRows: ScadaPropertyRow[]; constructor(protected store: Store, private fb: UntypedFormBuilder, - private http: HttpClient) { + private http: HttpClient, + private cd: ChangeDetectorRef) { } ngOnInit(): void { @@ -114,6 +132,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV this.scadaObjectSettingsFormGroup.disable({emitEvent: false}); } else { this.scadaObjectSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); } } @@ -122,10 +141,25 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV this.setupValue(); } + validate(c: UntypedFormControl) { + const valid = this.scadaObjectSettingsFormGroup.valid; + return valid ? null : { + scadaObject: { + valid: false, + }, + }; + } + private loadMetadata() { + if (this.validatorSubscription) { + this.validatorSubscription.unsubscribe(); + this.validatorSubscription = null; + } + this.validatorTriggers = []; this.http.get(this.svgPath, {responseType: 'text'}).subscribe( (svgContent) => { this.metadata = parseScadaObjectMetadataFromContent(svgContent); + this.propertyRows = toPropertyRows(this.metadata.properties); for (const control of Object.keys(this.scadaObjectSettingsFormGroup.controls)) { this.scadaObjectSettingsFormGroup.removeControl(control, {emitEvent: false}); } @@ -133,7 +167,15 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV this.scadaObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false}); } for (const property of this.metadata.properties) { + if (property.disableOnProperty) { + if (!this.validatorTriggers.includes(property.disableOnProperty)) { + this.validatorTriggers.push(property.disableOnProperty); + } + } const validators: ValidatorFn[] = []; + if (property.required) { + validators.push(Validators.required); + } if (property.type === 'number') { if (isDefinedAndNotNull(property.min)) { validators.push(Validators.min(property.min)); @@ -144,11 +186,37 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV } this.scadaObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); } + if (this.validatorTriggers.length) { + const observables: Observable[] = []; + for (const trigger of this.validatorTriggers) { + observables.push(this.scadaObjectSettingsFormGroup.get(trigger).valueChanges); + } + this.validatorSubscription = merge(...observables).subscribe(() => { + this.updateValidators(); + }); + } this.setupValue(); + this.cd.markForCheck(); } ); } + private updateValidators() { + for (const trigger of this.validatorTriggers) { + const value: boolean = this.scadaObjectSettingsFormGroup.get(trigger).value; + this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach( + (p) => { + const control = this.scadaObjectSettingsFormGroup.get(p.id); + if (value) { + control.enable({emitEvent: false}); + } else { + control.disable({emitEvent: false}); + } + } + ); + } + } + private setupValue() { if (this.metadata) { const defaults = defaultScadaObjectSettings(this.metadata); @@ -156,6 +224,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV this.scadaObjectSettingsFormGroup.patchValue( this.modelValue, {emitEvent: false} ); + this.setDisabledState(this.disabled); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts new file mode 100644 index 0000000000..2426b13a0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts @@ -0,0 +1,43 @@ +/// +/// Copyright ยฉ 2016-2024 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 { ScadaObjectProperty } from '@home/components/widget/lib/scada/scada.models'; + +export interface ScadaPropertyRow { + label: string; + properties: ScadaObjectProperty[]; + switch?: ScadaObjectProperty; +} + +export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => { + const result: ScadaPropertyRow[] = []; + for (const property of properties) { + let propertyRow = result.find(r => r.label === property.name); + if (!propertyRow) { + propertyRow = { + label: property.name, + properties: [] + }; + result.push(propertyRow); + } + if (property.type === 'switch') { + propertyRow.switch = property; + } else { + propertyRow.properties.push(property); + } + } + return result; +}; diff --git a/ui-ngx/src/assets/widget/scada/drawing.svg b/ui-ngx/src/assets/widget/scada/drawing.svg index 972ca550cd..03849049f3 100644 --- a/ui-ngx/src/assets/widget/scada/drawing.svg +++ b/ui-ngx/src/assets/widget/scada/drawing.svg @@ -1,39 +1,160 @@ - - { + { "title": "My first SCADA Object", - "initial": [ - { - "tag": "RECT", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "background" + "states": { + "initialState": { + "initial": true, + "state": [ + { + "tag": "level", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "levelBackground" + } + } + ] + }, + { + "tag": "levelValueBackground", + "show": { + "type": "property", + "propertyId": "showValue" } - }, - { - "name": "stroke", - "value": { - "type": "property", - "propertyId": "strokeColor" + }, + { + "tag": "levelValue", + "show": { + "type": "property", + "propertyId": "showValue" + }, + "text": { + "font": { + "type": "property", + "propertyId": "valueFont" + }, + "color": { + "type": "property", + "propertyId": "valueColor" + } } - }, - { - "name": "stroke-width", - "value": { - "type": "property", - "propertyId": "strokeWidth" + }, + { + "tag": "minLevel", + "show": { + "type": "property", + "propertyId": "showMinMaxLevel" + }, + "text": { + "content": { + "type": "property", + "propertyId": "minLevel" + }, + "font": { + "type": "property", + "propertyId": "minMaxLevelFont" + }, + "color": { + "type": "property", + "propertyId": "minMaxLevelColor" + } } - } - ] + }, + { + "tag": "maxLevel", + "show": { + "type": "property", + "propertyId": "showMinMaxLevel" + }, + "text": { + "content": { + "type": "property", + "propertyId": "maxLevel" + }, + "font": { + "type": "property", + "propertyId": "minMaxLevelFont" + }, + "color": { + "type": "property", + "propertyId": "minMaxLevelColor" + } + } + } + ] + }, + "updateLevelState": { + "state": [ + { + "tag": "level", + "animate": 200, + "attributes": [ + { + "name": "height", + "value": { + "type": "function", + "valueConvertFunction": "var level = (value - ${minLevel}) / (${maxLevel} - ${minLevel});\nlevel = Math.max(0, Math.min(1, level));\nreturn level*80;" + } + } + ] + }, + { + "tag": "levelValue", + "text": { + "content": { + "type": "valueFormat", + "units": { + "type": "property", + "propertyId": "valueUnits" + }, + "decimals": { + "type": "property", + "propertyId": "valueDecimals" + } + } + } + } + ] + }, + "disabledState": { + "state": [ + { + "tag": "level", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "disabledLevelBackground" + } + } + ] + } + ] + }, + "enabledState": { + "state": [ + { + "tag": "level", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "levelBackground" + } + } + ] + } + ] } - ], + }, "behavior": [ { - "id": "initialState", - "name": "Initial state", + "id": "levelState", + "name": "Level", "type": "getValue", "valueType": "DOUBLE", "defaultValue": 0, @@ -42,26 +163,7 @@ "matcher": { "type": "any" }, - "state": [ - { - "tag": "RECT", - "animate": 200, - "attributes": [ - { - "name": "height", - "value": { - "type": "input" - } - }, - { - "name": "width", - "value": { - "type": "input" - } - } - ] - } - ] + "stateId": "updateLevelState" } ] }, @@ -77,73 +179,129 @@ "type": "constant", "value": true }, - "state": [ - { - "tag": "RECT", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "disabledBackground" - } - } - ] - } - ] + "stateId": "disabledState" }, { "matcher": { "type": "constant", "value": false }, - "state": [ - { - "tag": "RECT", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "background" - } - } - ] - } - ] + "stateId": "enabledState" } ] } ], "properties": [ { - "id": "strokeColor", - "name": "Stroke color", - "type": "color", - "default": "#aaa" + "id": "showValue", + "name": "Value", + "type": "switch", + "default": true }, { - "id": "strokeWidth", - "name": "Stroke width", + "id": "valueUnits", + "name": "Value", + "type": "units", + "default": "", + "disableOnProperty": "showValue" + }, + { + "id": "valueDecimals", + "name": "Value", "type": "number", - "default": 5, + "default": 2, "min": 0, - "max": 20 + "max": 15, + "step": 1, + "fieldSuffix": "decimals", + "disableOnProperty": "showValue" + }, + { + "id": "valueFont", + "name": "Value", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showValue" + }, + { + "id": "valueColor", + "name": "Value", + "type": "color", + "default": "#000000", + "disableOnProperty": "showValue" + }, + { + "id": "minLevel", + "name": "Level range", + "subLabel": "min", + "type": "number", + "required": true, + "default": 0, + "min": 0 }, { - "id": "background", - "name": "Background", + "id": "maxLevel", + "name": "Level range", + "subLabel": "max", + "type": "number", + "required": true, + "default": 100, + "min": 0 + }, + { + "id": "showMinMaxLevel", + "name": "Min/Max label", + "type": "switch", + "default": true + }, + { + "id": "minMaxLevelFont", + "name": "Min/Max label", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showMinMaxLevel" + }, + { + "id": "minMaxLevelColor", + "name": "Min/Max label", + "type": "color", + "default": "#666", + "disableOnProperty": "showMinMaxLevel" + }, + { + "id": "levelBackground", + "name": "Level background", + "subLabel": "Enabled", "type": "color", - "default": "green" - }, - { - "id": "disabledBackground", - "name": "Disabled background", - "type": "color", - "default": "#ccc" - } + "default": "#1abb48", + "divider": true + }, + { + "id": "disabledLevelBackground", + "name": "Level background", + "subLabel": "Disabled", + "type": "color", + "default": "#ccc" + } ] - } - - + } + + + + min + + N/A + max From 937b48d85b8819df726e1c929f967de32d2071a3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 6 May 2024 19:30:31 +0300 Subject: [PATCH 005/331] UI: Scada test improvements, add actions. --- .../widget/lib/scada/scada.models.ts | 316 ++++++++++++++---- .../scada-object-settings.component.html | 27 +- .../scada/scada-object-settings.models.ts | 4 +- ui-ngx/src/assets/widget/scada/drawing.svg | 272 +++++++++++++-- 4 files changed, 520 insertions(+), 99 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts index 542e24844e..d0aeac27a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts @@ -16,7 +16,12 @@ import { ValueType } from '@shared/models/constants'; import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js'; -import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; +import { + DataToValueType, + GetValueAction, + GetValueSettings, SetValueAction, + SetValueSettings, ValueToDataType +} from '@shared/models/action-widget-settings.models'; import { formatValue, insertVariable, @@ -29,34 +34,10 @@ import { } from '@core/utils'; import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; import { map, share } from 'rxjs/operators'; -import { ValueAction, ValueGetter } from '@home/components/widget/lib/action/action-widget.models'; +import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { Font } from '@shared/models/widget-settings.models'; - -export type ValueMatcherType = 'any' | 'constant' | 'range'; - -export interface ValueMatcher { - type: ValueMatcherType; - value?: any; - range?: {from: number; to: number}; -} - -const valueMatches = (matcher: ValueMatcher, val: any): boolean => { - switch (matcher.type) { - case 'any': - return true; - case 'constant': - return matcher.value === val; - case 'range': - if (isDefinedAndNotNull(val) && isNumeric(val)) { - const num = Number(val); - const range = matcher.range; - return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)); - } else { - return false; - } - } -}; +import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat'; @@ -100,26 +81,28 @@ export interface ScadaObjectText { export interface ScadaObjectElementState { tag: string; + inputValue?: string; show?: ScadaObjectValue; text?: ScadaObjectText; attributes?: ScadaObjectAttribute[]; animate?: number; + addClass?: string; + removeClass?: string; + animationTimeline?: Timeline; } export interface ScadaObjectState { initial?: boolean; - value?: any; + triggerValues?: string[]; + conditionFunction?: string; + condition?: (values: {[key: string]: any}) => boolean; state: ScadaObjectElementState[]; } export interface ScadaObjectUpdateState { - matcher: ValueMatcher; - stateId: string; + updateValue: string; } -const filterUpdateStates = (states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] => - states.filter(s => valueMatches(s.matcher, val)); - export enum ScadaObjectBehaviorType { setValue = 'setValue', getValue = 'getValue' @@ -138,12 +121,14 @@ export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase { } export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase { - todo: any; + valueToDataType: ValueToDataType; + constantValue: any; + valueToDataFunction: string; } export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet; -export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'font' | 'units' | 'switch'; +export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; export interface ScadaObjectPropertyBase { id: string; @@ -155,6 +140,8 @@ export interface ScadaObjectPropertyBase { divider?: boolean; fieldSuffix?: string; disableOnProperty?: string; + rowClass?: string; + fieldClass?: string; } export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase { @@ -165,8 +152,38 @@ export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase { export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty; +export interface ScadaObjectCallBehavior { + behaviorId: string; + conditionFunction?: string; + condition?: (value: any) => boolean; +} + +export interface ScadaObjectStateValue { + initialValue: any; + callBehavior?: ScadaObjectCallBehavior[]; +} + +export type ScadaObjectElementActionTrigger = 'click'; + +export type ScadaObjectActionType = 'updateValue'; +export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant'; + +export interface ScadaObjectElementAction { + trigger: ScadaObjectElementActionTrigger; + enabledTriggerValues?: string[]; + enabledConditionFunction?: string; + enabledCondition?: (values: {[key: string]: any}) => boolean; + actionType: ScadaObjectActionType; + updateValueId?: string; + updateValueType?: ScadaObjectActionUpdateValueType; + updateValueConstant?: any; + updateValueInc?: number; +} + export interface ScadaObjectMetadata { title: string; + stateValues: {[id: string]: ScadaObjectStateValue}; + actions: {[tag: string]: ScadaObjectElementAction}; states: {[id: string]: ScadaObjectState}; behavior: ScadaObjectBehavior[]; properties: ScadaObjectProperty[]; @@ -174,6 +191,8 @@ export interface ScadaObjectMetadata { export const emptyMetadata: ScadaObjectMetadata = { title: '', + stateValues: {}, + actions: {}, states: {}, behavior: [], properties: [] @@ -211,7 +230,7 @@ const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings< requestTimeout: 5000, requestPersistent: false, persistentPollingInterval: 1000 - }, + }, getAttribute: { key: 'state', scope: null @@ -221,19 +240,41 @@ const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings< }, dataToValue: { type: DataToValueType.NONE, - compareToValue: true, - dataToValueFunction: '/* Should return boolean value */\nreturn data;' + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' } }); +const defaultSetValueSettings = (set: ScadaObjectBehaviorSet): SetValueSettings => ({ + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SERVER_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: set.valueToDataType, + constantValue: set.constantValue, + valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction : + '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' + } +}); + export const defaultScadaObjectSettings = (metadata: ScadaObjectMetadata): ScadaObjectSettings => { const settings: ScadaObjectSettings = {}; - for (const behaviour of metadata.behavior) { - //behaviour.id - if (behaviour.type === ScadaObjectBehaviorType.getValue) { - settings[behaviour.id] = defaultGetValueSettings(behaviour as ScadaObjectBehaviorGet); - } else if (behaviour.type === ScadaObjectBehaviorType.setValue) { - // TODO: + for (const behavior of metadata.behavior) { + if (behavior.type === ScadaObjectBehaviorType.getValue) { + settings[behavior.id] = defaultGetValueSettings(behavior as ScadaObjectBehaviorGet); + } else if (behavior.type === ScadaObjectBehaviorType.setValue) { + settings[behavior.id] = defaultSetValueSettings(behavior as ScadaObjectBehaviorSet); } } for (const property of metadata.properties) { @@ -258,8 +299,10 @@ export class ScadaObject { private loadingSubject = new BehaviorSubject(false); private valueGetters: ValueGetter[] = []; private valueActions: ValueAction[] = []; + private valueSetters: {[behaviorId: string]: ValueSetter} = {}; - private animationTimeline: Timeline; + private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; + private stateValues: {[id: string]: any} = {}; loading$ = this.loadingSubject.asObservable().pipe(share()); @@ -289,6 +332,10 @@ export class ScadaObject { } public destroy() { + for (const stateValueId of Object.keys(this.stateValueSubjects)) { + this.stateValueSubjects[stateValueId].complete(); + this.stateValueSubjects[stateValueId].unsubscribe(); + } this.valueActions.forEach(v => v.destroy()); this.loadingSubject.complete(); this.loadingSubject.unsubscribe(); @@ -303,8 +350,33 @@ export class ScadaObject { } private prepareMetadata() { + for (const stateValueId of Object.keys(this.metadata.stateValues)) { + const stateValue = this.metadata.stateValues[stateValueId]; + if (stateValue.callBehavior) { + stateValue.callBehavior.forEach(callBehavior => { + let condition = () => true; + if (callBehavior.conditionFunction) { + try { + condition = parseFunction(this.insertVariables(callBehavior.conditionFunction), ['value']); + } catch (e) { + condition = () => false; + } + } + callBehavior.condition = condition; + }); + } + } for (const stateId of Object.keys(this.metadata.states)) { const state = this.metadata.states[stateId]; + let condition = () => true; + if (state.conditionFunction) { + try { + condition = parseFunction(this.insertVariables(state.conditionFunction), ['values']); + } catch (e) { + condition = () => false; + } + } + state.condition = condition; for (const elementState of state.state) { this.prepareValue(elementState.show); this.prepareValue(elementState.text?.content); @@ -367,6 +439,35 @@ export class ScadaObject { } private initStates() { + for (const stateValueId of Object.keys(this.metadata.stateValues)) { + const stateValue = this.metadata.stateValues[stateValueId]; + this.stateValueSubjects[stateValueId] = new BehaviorSubject(stateValue.initialValue); + this.stateValues[stateValueId] = stateValue.initialValue; + this.stateValueSubjects[stateValueId].subscribe(val => { + this.stateValues[stateValueId] = val; + const states = Object.values(this.metadata.states); + const triggerStates = states.filter(s => s.triggerValues && s.triggerValues.includes(stateValueId)); + for (const state of triggerStates) { + if (state.condition(this.stateValues)) { + this.updateState(state); + } + } + }); + } + for (const tag of Object.keys(this.metadata.actions)) { + const action = this.metadata.actions[tag]; + const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); + switch (action.trigger) { + case 'click': + elements.forEach(e => { + e.attr('cursor', 'pointer'); + e.on('click', () => { + this.triggerAction(action); + }); + }); + break; + } + } for (const behavior of this.metadata.behavior) { if (behavior.type === ScadaObjectBehaviorType.getValue) { const getBehavior = behavior as ScadaObjectBehaviorGet; @@ -379,6 +480,13 @@ export class ScadaObject { }); this.valueGetters.push(valueGetter); this.valueActions.push(valueGetter); + } else if (behavior.type === ScadaObjectBehaviorType.setValue) { + const setBehavior = behavior as ScadaObjectBehaviorSet; + let setValueSettings: SetValueSettings = this.settings[setBehavior.id]; + setValueSettings = {...setValueSettings, actionLabel: setBehavior.name}; + const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings); + this.valueSetters[setBehavior.id] = valueSetter; + this.valueActions.push(valueSetter); } } const initialState = Object.values(this.metadata.states).find(s => s.initial); @@ -404,6 +512,59 @@ export class ScadaObject { } } + private triggerAction(action: ScadaObjectElementAction) { + switch (action.actionType) { + case 'updateValue': + const targetValue = action.updateValueId; + const valueSubject = this.stateValueSubjects[targetValue]; + if (valueSubject) { + const currentVal = valueSubject.value; + let newValue: any; + switch (action.updateValueType) { + case 'toggle': + newValue = !currentVal; + break; + case 'increment': + if (isNumber(currentVal)) { + newValue = currentVal + action.updateValueInc; + } + break; + case 'constant': + newValue = action.updateValueConstant; + break; + } + if (isDefinedAndNotNull(newValue)) { + valueSubject.next(newValue); + const stateValue = this.metadata.stateValues[targetValue]; + if (stateValue.callBehavior) { + const observables: Observable[] = []; + for (const behavior of stateValue.callBehavior) { + if (behavior.condition(newValue)) { + const valueSetter = this.valueSetters[behavior.behaviorId]; + observables.push(valueSetter.setValue(newValue)); + } + } + if (observables.length) { + this.loadingSubject.next(true); + forkJoin(observables).subscribe( + { + next: () => { + this.loadingSubject.next(false); + }, + error: (err) => { + this.loadingSubject.next(false); + valueSubject.next(currentVal); + } + } + ); + } + } + } + } + break; + } + } + private resize() { let scale: number; if (this.targetWidth < this.targetHeight) { @@ -417,20 +578,23 @@ export class ScadaObject { private onValue(id: string, value: any) { const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet; value = this.normalizeValue(value, getBehavior.valueType); - const updateStates = filterUpdateStates(getBehavior.onUpdate, value); - if (this.animationTimeline) { - this.animationTimeline.finish(); - } - for (const updateState of updateStates) { - const state = this.metadata.states[updateState.stateId]; - this.updateState(state, value); + for (const onUpdate of getBehavior.onUpdate) { + const targetStateValueId = onUpdate.updateValue; + this.stateValueSubjects[targetStateValueId].next(value); } } - private updateState(state: ScadaObjectState, value?: any) { + private updateState(state: ScadaObjectState) { if (state) { for (const elementState of state.state) { const tag = elementState.tag; + let value; + if (elementState.inputValue) { + value = this.stateValues[elementState.inputValue]; + } + if (elementState.animationTimeline) { + elementState.animationTimeline.finish(); + } const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); if (elements.length) { if (elementState.show) { @@ -443,10 +607,20 @@ export class ScadaObject { } }); } + if (elementState.addClass) { + elements.forEach(e => { + e.addClass(elementState.addClass); + }); + } + if (elementState.removeClass) { + elements.forEach(e => { + e.removeClass(elementState.removeClass); + }); + } if (elementState.attributes) { const attrs = this.computeAttributes(elementState.attributes, value); elements.forEach(e => { - this.setElementAttributes(e, attrs, elementState.animate); + this.setElementAttributes(elementState, e, attrs, elementState.animate); }); } if (elementState.text) { @@ -502,9 +676,9 @@ export class ScadaObject { return res; } - private setElementAttributes(element: Element, attrs: {[attr: string]: any}, animate?: number) { + private setElementAttributes(elementState: ScadaObjectElementState, element: Element, attrs: {[attr: string]: any}, animate?: number) { if (isDefinedAndNotNull(animate)) { - this.animation(element, animate).attr(attrs); + this.animation(elementState, element, animate).attr(attrs); } else { element.attr(attrs); } @@ -545,11 +719,11 @@ export class ScadaObject { } } - private animation(element: Element, duration: number): Runner { - if (!this.animationTimeline) { - this.animationTimeline = new Timeline(); + private animation(elementState: ScadaObjectElementState, element: Element, duration: number): Runner { + if (!elementState.animationTimeline) { + elementState.animationTimeline = new Timeline(); } - element.timeline(this.animationTimeline); + element.timeline(elementState.animationTimeline); return element.animate(duration, 0, 'now'); } @@ -561,7 +735,14 @@ export class ScadaObject { case 'constant': return objectValue.constantValue; case 'property': - return objectValue.computedPropertyValue; + const property = this.getProperty(objectValue.propertyId); + if (property.type === 'color-settings') { + const colorProcessor: ColorProcessor = objectValue.computedPropertyValue; + colorProcessor.update(value); + return colorProcessor.color; + } else { + return objectValue.computedPropertyValue; + } case 'function': try { return objectValue.valueConverter(value); @@ -584,11 +765,18 @@ export class ScadaObject { } } + private getProperty(id: string): ScadaObjectProperty { + return this.metadata.properties.find(p => p.id === id); + } + private getPropertyValue(id: string): any { - const property = this.metadata.properties.find(p => p.id === id); + const property = this.getProperty(id); if (property) { const value = this.settings[id]; if (isDefinedAndNotNull(value)) { + if (property.type === 'color-settings') { + return ColorProcessor.fromSettings(value); + } return value; } else { switch (property.type) { @@ -598,6 +786,8 @@ export class ScadaObject { return 0; case 'color': return '#000'; + case 'color-settings': + return constantColor('#000'); } } } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html index 2907a951dd..972a5c532c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html @@ -30,39 +30,54 @@ [widgetType]="widgetType" formControlName="{{ behaviour.id }}"> +
-
+
{{ propertyRow.label }}
{{ propertyRow.label }}
-
+
{{ property.subLabel }}
- + -
{{ property.fieldSuffix }}
+
{{ property.fieldSuffix }}
- + + + -
{{ property.fieldSuffix }}
+
{{ property.fieldSuffix }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts index 2426b13a0b..07009a98cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts @@ -20,6 +20,7 @@ export interface ScadaPropertyRow { label: string; properties: ScadaObjectProperty[]; switch?: ScadaObjectProperty; + rowClass?: string; } export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => { @@ -29,7 +30,8 @@ export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaProperty if (!propertyRow) { propertyRow = { label: property.name, - properties: [] + properties: [], + rowClass: property.rowClass }; result.push(propertyRow); } diff --git a/ui-ngx/src/assets/widget/scada/drawing.svg b/ui-ngx/src/assets/widget/scada/drawing.svg index 03849049f3..61e8d3f1ca 100644 --- a/ui-ngx/src/assets/widget/scada/drawing.svg +++ b/ui-ngx/src/assets/widget/scada/drawing.svg @@ -1,12 +1,71 @@ { "title": "My first SCADA Object", + "stateValues": { + "level": { + "initialValue": 0 + }, + "disabled": { + "initialValue": false + }, + "on": { + "initialValue": false, + "callBehavior": [ + { + "behaviorId": "onUpdateState", + "conditionFunction": "return value === true;" + }, + { + "behaviorId": "offUpdateState", + "conditionFunction": "return value === false;" + } + ] + } + }, + "actions": { + "onButton": { + "trigger": "click", + "actionType": "updateValue", + "updateValueId": "on", + "updateValueType": "constant", + "updateValueConstant": true + }, + "offButton": { + "trigger": "click", + "actionType": "updateValue", + "updateValueId": "on", + "updateValueType": "constant", + "updateValueConstant": false + } + }, "states": { "initialState": { "initial": true, "state": [ + { + "tag": "levelTitle", + "show": { + "type": "property", + "propertyId": "showLevelTitle" + }, + "text": { + "content": { + "type": "property", + "propertyId": "levelTitle" + }, + "font": { + "type": "property", + "propertyId": "levelTitleFont" + }, + "color": { + "type": "property", + "propertyId": "levelTitleColor" + } + } + }, { "tag": "level", + "inputValue": "level", "attributes": [ { "name": "fill", @@ -86,9 +145,11 @@ ] }, "updateLevelState": { + "triggerValues": ["level"], "state": [ { "tag": "level", + "inputValue": "level", "animate": 200, "attributes": [ { @@ -102,6 +163,7 @@ }, { "tag": "levelValue", + "inputValue": "level", "text": { "content": { "type": "valueFormat", @@ -118,7 +180,28 @@ } ] }, + "updateLevelBackgroundState": { + "triggerValues": ["disabled", "level"], + "conditionFunction": "return values.disabled === false;", + "state": [ + { + "tag": "level", + "inputValue": "level", + "attributes": [ + { + "name": "fill", + "value": { + "type": "property", + "propertyId": "levelBackground" + } + } + ] + } + ] + }, "disabledState": { + "triggerValues": ["disabled"], + "conditionFunction": "return values.disabled === true;", "state": [ { "tag": "level", @@ -134,16 +217,68 @@ } ] }, - "enabledState": { + "onState": { + "triggerValues": ["on"], + "conditionFunction": "return values.on === true;", "state": [ { - "tag": "level", + "tag": "fan", + "addClass": "fan-rotate" + }, + { + "tag": "onButtonBackground", "attributes": [ { "name": "fill", "value": { - "type": "property", - "propertyId": "levelBackground" + "type": "constant", + "constantValue": "#777" + } + } + ] + }, + { + "tag": "offButtonBackground", + "attributes": [ + { + "name": "fill", + "value": { + "type": "constant", + "constantValue": "#ed121f" + } + } + ] + } + ] + }, + "offState": { + "triggerValues": ["on"], + "conditionFunction": "return values.on === false;", + "state": [ + { + "tag": "fan", + "removeClass": "fan-rotate" + }, + { + "tag": "onButtonBackground", + "attributes": [ + { + "name": "fill", + "value": { + "type": "constant", + "constantValue": "#12ed19" + } + } + ] + }, + { + "tag": "offButtonBackground", + "attributes": [ + { + "name": "fill", + "value": { + "type": "constant", + "constantValue": "#777" } } ] @@ -160,10 +295,7 @@ "defaultValue": 0, "onUpdate": [ { - "matcher": { - "type": "any" - }, - "stateId": "updateLevelState" + "updateValue": "level" } ] }, @@ -175,35 +307,87 @@ "defaultValue": false, "onUpdate": [ { - "matcher": { - "type": "constant", - "value": true - }, - "stateId": "disabledState" - }, + "updateValue": "disabled" + } + ] + }, + { + "id": "onState", + "name": "On/Off state", + "type": "getValue", + "valueType": "BOOLEAN", + "defaultValue": false, + "onUpdate": [ { - "matcher": { - "type": "constant", - "value": false - }, - "stateId": "enabledState" + "updateValue": "on" } ] + }, + { + "id": "onUpdateState", + "name": "Power On", + "type": "setValue", + "valueToDataType": "CONSTANT", + "constantValue": true + }, + { + "id": "offUpdateState", + "name": "Power Off", + "type": "setValue", + "valueToDataType": "CONSTANT", + "constantValue": false } ], "properties": [ + { + "id": "showLevelTitle", + "name": "Level title", + "type": "switch", + "default": true, + "rowClass": "column-xs" + }, + { + "id": "levelTitle", + "name": "Level title", + "type": "string", + "default": "Level", + "disableOnProperty": "showLevelTitle", + "fieldClass": "flex" + }, + { + "id": "levelTitleFont", + "name": "Level title", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showLevelTitle" + }, + { + "id": "levelTitleColor", + "name": "Level title", + "type": "color", + "default": "#000000", + "disableOnProperty": "showLevelTitle" + }, { "id": "showValue", "name": "Value", "type": "switch", - "default": true + "default": true, + "rowClass": "column-xs" }, { "id": "valueUnits", "name": "Value", "type": "units", "default": "", - "disableOnProperty": "showValue" + "disableOnProperty": "showValue", + "fieldClass": "flex" }, { "id": "valueDecimals", @@ -214,7 +398,8 @@ "max": 15, "step": 1, "fieldSuffix": "decimals", - "disableOnProperty": "showValue" + "disableOnProperty": "showValue", + "fieldClass": "flex" }, { "id": "valueFont", @@ -243,7 +428,9 @@ "type": "number", "required": true, "default": 0, - "min": 0 + "min": 0, + "rowClass": "column-xs", + "fieldClass": "flex-xs" }, { "id": "maxLevel", @@ -252,7 +439,8 @@ "type": "number", "required": true, "default": 100, - "min": 0 + "min": 0, + "fieldClass": "flex-xs" }, { "id": "showMinMaxLevel", @@ -284,9 +472,14 @@ "id": "levelBackground", "name": "Level background", "subLabel": "Enabled", - "type": "color", - "default": "#1abb48", - "divider": true + "type": "color-settings", + "default": { + "type": "constant", + "color": "#1abb48", + "colorFunction": "return value > 70 ? '#d5280d' : '#1abb48';" + }, + "divider": true, + "rowClass": "column-xs" }, { "id": "disabledLevelBackground", @@ -297,11 +490,32 @@ } ] } + min - N/A - max + + N/A + Level + max + + + + + + On + + + + Off + From ebce8698e346eab5cf1fe5c4369850573f7cc664 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 7 May 2024 19:58:48 +0300 Subject: [PATCH 006/331] UI: Refactoring. --- ui-ngx/src/app/core/api/widget-api.models.ts | 3 +- .../basic/basic-widget-config.module.ts | 10 +- .../scada-test-basic-config.component.html | 26 - .../svg/iot-svg-basic-config.component.html | 48 ++ .../iot-svg-basic-config.component.ts} | 55 +- .../widget/lib/scada/scada.models.ts | 797 ------------------ .../iot-svg-object-settings.component.html} | 15 +- .../iot-svg-object-settings.component.ts} | 34 +- .../iot-svg-object-settings.models.ts} | 8 +- .../common/widget-settings-common.module.ts | 8 +- .../iot-svg-widget.component.html} | 9 +- .../iot-svg-widget.component.scss} | 34 +- .../iot-svg-widget.component.ts} | 59 +- .../iot-svg-widget.models.ts} | 23 +- .../widget/lib/svg/iot-svg.models.ts | 634 ++++++++++++++ .../widget/widget-components.module.ts | 6 +- .../components/widget/widget.component.ts | 35 +- .../assets/widget/{scada => svg}/drawing.svg | 382 ++------- 18 files changed, 970 insertions(+), 1216 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html rename ui-ngx/src/app/modules/home/components/widget/config/basic/{scada/scada-test-basic-config.component.ts => svg/iot-svg-basic-config.component.ts} (50%) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{scada/scada-object-settings.component.html => svg/iot-svg-object-settings.component.html} (87%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{scada/scada-object-settings.component.ts => svg/iot-svg-object-settings.component.ts} (88%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{scada/scada-object-settings.models.ts => svg/iot-svg-object-settings.models.ts} (82%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.component.html => svg/iot-svg-widget.component.html} (57%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.component.scss => svg/iot-svg-widget.component.scss} (54%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.component.ts => svg/iot-svg-widget.component.ts} (51%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.models.ts => svg/iot-svg-widget.models.ts} (50%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts rename ui-ngx/src/assets/widget/{scada => svg}/drawing.svg (54%) diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 1fcd18bd93..0300c89d7f 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -23,7 +23,7 @@ import { DatasourceType, KeyInfo, LegendConfig, - LegendData, TargetDevice, + LegendData, TargetDevice, WidgetAction, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; @@ -95,6 +95,7 @@ export interface WidgetActionsApi { getActionDescriptors: (actionSourceId: string) => Array; handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; + onWidgetAction: ($event: Event, action: WidgetAction) => void; elementClick: ($event: Event) => void; cardClick: ($event: Event) => void; click: ($event: Event) => void; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 79af9bee18..6f83f9ac25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -129,9 +129,7 @@ import { import { RadarChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/radar-chart-basic-config.component'; -import { - ScadaTestBasicConfigComponent -} from '@home/components/widget/config/basic/scada/scada-test-basic-config.component'; +import { IotSvgBasicConfigComponent } from '@home/components/widget/config/basic/svg/iot-svg-basic-config.component'; @NgModule({ declarations: [ @@ -175,7 +173,7 @@ import { BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, - ScadaTestBasicConfigComponent + IotSvgBasicConfigComponent ], imports: [ CommonModule, @@ -221,7 +219,7 @@ import { BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, - ScadaTestBasicConfigComponent + IotSvgBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -261,5 +259,5 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type - - - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html new file mode 100644 index 0000000000..f0dfeacf66 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html @@ -0,0 +1,48 @@ + + + + + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts similarity index 50% rename from ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts rename to ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts index 3d6a4a6a45..cccdf03aba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts @@ -20,22 +20,26 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { TargetDevice, } from '@shared/models/widget.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models'; +import { + iotSvgWidgetDefaultSettings, + IotSvgWidgetSettings +} from '@home/components/widget/lib/svg/iot-svg-widget.models'; +import { isUndefined } from '@core/utils'; @Component({ - selector: 'tb-scada-test-basic-config', - templateUrl: './scada-test-basic-config.component.html', + selector: 'tb-iot-svg-basic-config', + templateUrl: './iot-svg-basic-config.component.html', styleUrls: ['../basic-config.scss'] }) -export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent { +export class IotSvgBasicConfigComponent extends BasicWidgetConfigComponent { get targetDevice(): TargetDevice { - return this.scadaTestWidgetConfigForm.get('targetDevice').value; + return this.iotSvgWidgetConfigForm.get('targetDevice').value; } - scadaTestWidgetConfigForm: UntypedFormGroup; + iotSvgWidgetConfigForm: UntypedFormGroup; constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, @@ -44,21 +48,48 @@ export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent { } protected configForm(): UntypedFormGroup { - return this.scadaTestWidgetConfigForm; + return this.iotSvgWidgetConfigForm; } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: ScadaTestWidgetSettings = {...(configData.config.settings as ScadaTestWidgetSettings || { scadaObject: {}})}; - this.scadaTestWidgetConfigForm = this.fb.group({ + const settings: IotSvgWidgetSettings = {...iotSvgWidgetDefaultSettings, ...(configData.config.settings || {})}; + this.iotSvgWidgetConfigForm = this.fb.group({ targetDevice: [configData.config.targetDevice, []], - scadaObject: [settings.scadaObject, []] + iotSvg: [settings.iotSvg, []], + iotSvgObject: [settings.iotSvgObject, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] }); } protected prepareOutputConfig(config: any): WidgetConfigComponentData { this.widgetConfig.config.targetDevice = config.targetDevice; this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; - this.widgetConfig.config.settings.scadaObject = config.scadaObject; + this.widgetConfig.config.settings.iotSvg = config.iotSvg; + this.widgetConfig.config.settings.iotSvgObject = config.iotSvgObject; + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; return this.widgetConfig; } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts deleted file mode 100644 index d0aeac27a7..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts +++ /dev/null @@ -1,797 +0,0 @@ -/// -/// Copyright ยฉ 2016-2024 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 { ValueType } from '@shared/models/constants'; -import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js'; -import { - DataToValueType, - GetValueAction, - GetValueSettings, SetValueAction, - SetValueSettings, ValueToDataType -} from '@shared/models/action-widget-settings.models'; -import { - formatValue, - insertVariable, - isDefinedAndNotNull, - isNumber, - isNumeric, - isUndefinedOrNull, - mergeDeep, - parseFunction -} from '@core/utils'; -import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; -import { map, share } from 'rxjs/operators'; -import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; -import { WidgetContext } from '@home/models/widget-component.models'; -import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; - -export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat'; - -export interface ScadaObjectValueBase { - type: ScadaObjectValueType; -} - -export interface ScadaObjectValueConstant extends ScadaObjectValueBase { - constantValue?: any; -} - -export interface ScadaObjectValueProperty extends ScadaObjectValueBase { - propertyId?: string; - computedPropertyValue?: any; -} - -export interface ScadaObjectValueFunction extends ScadaObjectValueBase { - valueConvertFunction?: string; - valueConverter?: (val: any) => any; -} - -export interface ScadaObjectValueFormat extends ScadaObjectValueBase { - units?: ScadaObjectValue; - decimals?: ScadaObjectValue; - computedUnits?: string; - computedDecimals?: number; -} - -export type ScadaObjectValue = ScadaObjectValueProperty & ScadaObjectValueConstant & ScadaObjectValueFunction & ScadaObjectValueFormat; - -export interface ScadaObjectAttribute { - name: string; - value: ScadaObjectValue; -} - -export interface ScadaObjectText { - content?: ScadaObjectValue; - font?: ScadaObjectValue; - color?: ScadaObjectValue; -} - -export interface ScadaObjectElementState { - tag: string; - inputValue?: string; - show?: ScadaObjectValue; - text?: ScadaObjectText; - attributes?: ScadaObjectAttribute[]; - animate?: number; - addClass?: string; - removeClass?: string; - animationTimeline?: Timeline; -} - -export interface ScadaObjectState { - initial?: boolean; - triggerValues?: string[]; - conditionFunction?: string; - condition?: (values: {[key: string]: any}) => boolean; - state: ScadaObjectElementState[]; -} - -export interface ScadaObjectUpdateState { - updateValue: string; -} - -export enum ScadaObjectBehaviorType { - setValue = 'setValue', - getValue = 'getValue' -} - -export interface ScadaObjectBehaviorBase { - id: string; - name: string; - type: ScadaObjectBehaviorType; -} - -export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase { - valueType: ValueType; - defaultValue: any; - onUpdate: ScadaObjectUpdateState[]; -} - -export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase { - valueToDataType: ValueToDataType; - constantValue: any; - valueToDataFunction: string; -} - -export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet; - -export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; - -export interface ScadaObjectPropertyBase { - id: string; - name: string; - type: ScadaObjectPropertyType; - default: any; - required?: boolean; - subLabel?: string; - divider?: boolean; - fieldSuffix?: string; - disableOnProperty?: string; - rowClass?: string; - fieldClass?: string; -} - -export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase { - min?: number; - max?: number; - step?: number; -} - -export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty; - -export interface ScadaObjectCallBehavior { - behaviorId: string; - conditionFunction?: string; - condition?: (value: any) => boolean; -} - -export interface ScadaObjectStateValue { - initialValue: any; - callBehavior?: ScadaObjectCallBehavior[]; -} - -export type ScadaObjectElementActionTrigger = 'click'; - -export type ScadaObjectActionType = 'updateValue'; -export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant'; - -export interface ScadaObjectElementAction { - trigger: ScadaObjectElementActionTrigger; - enabledTriggerValues?: string[]; - enabledConditionFunction?: string; - enabledCondition?: (values: {[key: string]: any}) => boolean; - actionType: ScadaObjectActionType; - updateValueId?: string; - updateValueType?: ScadaObjectActionUpdateValueType; - updateValueConstant?: any; - updateValueInc?: number; -} - -export interface ScadaObjectMetadata { - title: string; - stateValues: {[id: string]: ScadaObjectStateValue}; - actions: {[tag: string]: ScadaObjectElementAction}; - states: {[id: string]: ScadaObjectState}; - behavior: ScadaObjectBehavior[]; - properties: ScadaObjectProperty[]; -} - -export const emptyMetadata: ScadaObjectMetadata = { - title: '', - stateValues: {}, - actions: {}, - states: {}, - behavior: [], - properties: [] -}; - - -export const parseScadaObjectMetadataFromContent = (svgContent: string): ScadaObjectMetadata => { - try { - const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); - return parseScadaObjectMetadataFromDom(svgDoc); - } catch (_e) { - return emptyMetadata; - } -}; - -const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata => { - try { - const elements = svgDoc.getElementsByTagName('tb:metadata'); - if (elements.length) { - return JSON.parse(elements[0].innerHTML); - } else { - return emptyMetadata; - } - } catch (_e) { - console.error(_e); - return emptyMetadata; - } -}; - -const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings => ({ - action: GetValueAction.DO_NOTHING, - defaultValue: get.defaultValue, - executeRpc: { - method: 'getState', - requestTimeout: 5000, - requestPersistent: false, - persistentPollingInterval: 1000 - }, - getAttribute: { - key: 'state', - scope: null - }, - getTimeSeries: { - key: 'state' - }, - dataToValue: { - type: DataToValueType.NONE, - compareToValue: true, - dataToValueFunction: '/* Should return boolean value */\nreturn data;' - } - }); - -const defaultSetValueSettings = (set: ScadaObjectBehaviorSet): SetValueSettings => ({ - action: SetValueAction.EXECUTE_RPC, - executeRpc: { - method: 'setState', - requestTimeout: 5000, - requestPersistent: false, - persistentPollingInterval: 1000 - }, - setAttribute: { - key: 'state', - scope: AttributeScope.SERVER_SCOPE - }, - putTimeSeries: { - key: 'state' - }, - valueToData: { - type: set.valueToDataType, - constantValue: set.constantValue, - valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction : - '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' - } -}); - -export const defaultScadaObjectSettings = (metadata: ScadaObjectMetadata): ScadaObjectSettings => { - const settings: ScadaObjectSettings = {}; - for (const behavior of metadata.behavior) { - if (behavior.type === ScadaObjectBehaviorType.getValue) { - settings[behavior.id] = defaultGetValueSettings(behavior as ScadaObjectBehaviorGet); - } else if (behavior.type === ScadaObjectBehaviorType.setValue) { - settings[behavior.id] = defaultSetValueSettings(behavior as ScadaObjectBehaviorSet); - } - } - for (const property of metadata.properties) { - settings[property.id] = property.default; - } - return settings; -}; - -export type ScadaObjectSettings = {[id: string]: any}; - -export class ScadaObject { - - private metadata: ScadaObjectMetadata; - private settings: ScadaObjectSettings; - - private rootElement: HTMLElement; - private svgShape: Svg; - private box: Box; - private targetWidth: number; - private targetHeight: number; - - private loadingSubject = new BehaviorSubject(false); - private valueGetters: ValueGetter[] = []; - private valueActions: ValueAction[] = []; - private valueSetters: {[behaviorId: string]: ValueSetter} = {}; - - private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; - private stateValues: {[id: string]: any} = {}; - - loading$ = this.loadingSubject.asObservable().pipe(share()); - - constructor(private ctx: WidgetContext, - private svgPath: string, - private inputSettings: ScadaObjectSettings) {} - - public init(): Observable { - return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( - map((inputSvgContent) => { - const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); - this.metadata = parseScadaObjectMetadataFromDom(doc); - const defaults = defaultScadaObjectSettings(this.metadata); - this.settings = mergeDeep({}, defaults, this.inputSettings || {}); - this.prepareMetadata(); - this.prepareSvgShape(doc); - this.initStates(); - }) - ); - } - - public addTo(element: HTMLElement) { - this.rootElement = element; - if (this.svgShape) { - this.svgShape.addTo(element); - } - } - - public destroy() { - for (const stateValueId of Object.keys(this.stateValueSubjects)) { - this.stateValueSubjects[stateValueId].complete(); - this.stateValueSubjects[stateValueId].unsubscribe(); - } - this.valueActions.forEach(v => v.destroy()); - this.loadingSubject.complete(); - this.loadingSubject.unsubscribe(); - } - - public setSize(targetWidth: number, targetHeight: number) { - this.targetWidth = targetWidth; - this.targetHeight = targetHeight; - if (this.svgShape) { - this.resize(); - } - } - - private prepareMetadata() { - for (const stateValueId of Object.keys(this.metadata.stateValues)) { - const stateValue = this.metadata.stateValues[stateValueId]; - if (stateValue.callBehavior) { - stateValue.callBehavior.forEach(callBehavior => { - let condition = () => true; - if (callBehavior.conditionFunction) { - try { - condition = parseFunction(this.insertVariables(callBehavior.conditionFunction), ['value']); - } catch (e) { - condition = () => false; - } - } - callBehavior.condition = condition; - }); - } - } - for (const stateId of Object.keys(this.metadata.states)) { - const state = this.metadata.states[stateId]; - let condition = () => true; - if (state.conditionFunction) { - try { - condition = parseFunction(this.insertVariables(state.conditionFunction), ['values']); - } catch (e) { - condition = () => false; - } - } - state.condition = condition; - for (const elementState of state.state) { - this.prepareValue(elementState.show); - this.prepareValue(elementState.text?.content); - this.prepareValue(elementState.text?.font); - this.prepareValue(elementState.text?.color); - if (elementState.attributes) { - for (const attribute of elementState.attributes) { - this.prepareValue(attribute.value); - } - } - } - } - } - - private prepareValue(value: ScadaObjectValue) { - if (value) { - if (value.type === 'function' && value.valueConvertFunction) { - try { - value.valueConverter = parseFunction(this.insertVariables(value.valueConvertFunction), ['value']); - } catch (e) { - value.valueConverter = (v) => v; - } - } else if (value.type === 'property') { - value.computedPropertyValue = this.getPropertyValue(value.propertyId); - } else if (value.type === 'valueFormat') { - if (value.units) { - this.prepareValue(value.units); - } - if (value.decimals) { - this.prepareValue(value.decimals); - } - } - } - } - - private insertVariables(content: string): string { - for (const property of this.metadata.properties) { - const value = this.getPropertyValue(property.id); - content = insertVariable(content, property.id, value); - } - return content; - } - - private prepareSvgShape(doc: XMLDocument) { - const elements = doc.getElementsByTagName('tb:metadata'); - for (let i=0;i(stateValue.initialValue); - this.stateValues[stateValueId] = stateValue.initialValue; - this.stateValueSubjects[stateValueId].subscribe(val => { - this.stateValues[stateValueId] = val; - const states = Object.values(this.metadata.states); - const triggerStates = states.filter(s => s.triggerValues && s.triggerValues.includes(stateValueId)); - for (const state of triggerStates) { - if (state.condition(this.stateValues)) { - this.updateState(state); - } - } - }); - } - for (const tag of Object.keys(this.metadata.actions)) { - const action = this.metadata.actions[tag]; - const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); - switch (action.trigger) { - case 'click': - elements.forEach(e => { - e.attr('cursor', 'pointer'); - e.on('click', () => { - this.triggerAction(action); - }); - }); - break; - } - } - for (const behavior of this.metadata.behavior) { - if (behavior.type === ScadaObjectBehaviorType.getValue) { - const getBehavior = behavior as ScadaObjectBehaviorGet; - let getValueSettings: GetValueSettings = this.settings[getBehavior.id]; - getValueSettings = {...getValueSettings, actionLabel: getBehavior.name}; - const valueGetter = - ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, { - next: (val) => {this.onValue(getBehavior.id, val);}, - error: (e) => {} - }); - this.valueGetters.push(valueGetter); - this.valueActions.push(valueGetter); - } else if (behavior.type === ScadaObjectBehaviorType.setValue) { - const setBehavior = behavior as ScadaObjectBehaviorSet; - let setValueSettings: SetValueSettings = this.settings[setBehavior.id]; - setValueSettings = {...setValueSettings, actionLabel: setBehavior.name}; - const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings); - this.valueSetters[setBehavior.id] = valueSetter; - this.valueActions.push(valueSetter); - } - } - const initialState = Object.values(this.metadata.states).find(s => s.initial); - if (initialState) { - this.updateState(initialState); - } - if (this.valueGetters.length) { - const getValueObservables: Array> = []; - this.valueGetters.forEach(valueGetter => { - getValueObservables.push(valueGetter.getValue()); - }); - this.loadingSubject.next(true); - forkJoin(getValueObservables).subscribe( - { - next: () => { - this.loadingSubject.next(false); - }, - error: () => { - this.loadingSubject.next(false); - } - } - ); - } - } - - private triggerAction(action: ScadaObjectElementAction) { - switch (action.actionType) { - case 'updateValue': - const targetValue = action.updateValueId; - const valueSubject = this.stateValueSubjects[targetValue]; - if (valueSubject) { - const currentVal = valueSubject.value; - let newValue: any; - switch (action.updateValueType) { - case 'toggle': - newValue = !currentVal; - break; - case 'increment': - if (isNumber(currentVal)) { - newValue = currentVal + action.updateValueInc; - } - break; - case 'constant': - newValue = action.updateValueConstant; - break; - } - if (isDefinedAndNotNull(newValue)) { - valueSubject.next(newValue); - const stateValue = this.metadata.stateValues[targetValue]; - if (stateValue.callBehavior) { - const observables: Observable[] = []; - for (const behavior of stateValue.callBehavior) { - if (behavior.condition(newValue)) { - const valueSetter = this.valueSetters[behavior.behaviorId]; - observables.push(valueSetter.setValue(newValue)); - } - } - if (observables.length) { - this.loadingSubject.next(true); - forkJoin(observables).subscribe( - { - next: () => { - this.loadingSubject.next(false); - }, - error: (err) => { - this.loadingSubject.next(false); - valueSubject.next(currentVal); - } - } - ); - } - } - } - } - break; - } - } - - private resize() { - let scale: number; - if (this.targetWidth < this.targetHeight) { - scale = this.targetWidth / this.box.width; - } else { - scale = this.targetHeight / this.box.height; - } - this.svgShape.node.style.transform = `scale(${scale})`; - } - - private onValue(id: string, value: any) { - const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet; - value = this.normalizeValue(value, getBehavior.valueType); - for (const onUpdate of getBehavior.onUpdate) { - const targetStateValueId = onUpdate.updateValue; - this.stateValueSubjects[targetStateValueId].next(value); - } - } - - private updateState(state: ScadaObjectState) { - if (state) { - for (const elementState of state.state) { - const tag = elementState.tag; - let value; - if (elementState.inputValue) { - value = this.stateValues[elementState.inputValue]; - } - if (elementState.animationTimeline) { - elementState.animationTimeline.finish(); - } - const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); - if (elements.length) { - if (elementState.show) { - const show: boolean = this.computeValue(elementState.show, value); - elements.forEach(e => { - if (show) { - e.show(); - } else { - e.hide(); - } - }); - } - if (elementState.addClass) { - elements.forEach(e => { - e.addClass(elementState.addClass); - }); - } - if (elementState.removeClass) { - elements.forEach(e => { - e.removeClass(elementState.removeClass); - }); - } - if (elementState.attributes) { - const attrs = this.computeAttributes(elementState.attributes, value); - elements.forEach(e => { - this.setElementAttributes(elementState, e, attrs, elementState.animate); - }); - } - if (elementState.text) { - if (elementState.text.content) { - const text: string = this.computeValue(elementState.text.content, value); - elements.forEach(e => { - this.setElementText(e, text); - }); - } - if (elementState.text.font || elementState.text.color) { - let font: Font = this.computeValue(elementState.text.font, value); - if (typeof font !== 'object') { - font = undefined; - } - let color: string = this.computeValue(elementState.text.color, value); - if (typeof color !== 'string') { - color = undefined; - } - elements.forEach(e => { - this.setElementFont(e, font, color); - }); - } - } - } - } - } - } - - private normalizeValue(value: any, type: ValueType): any { - if (isUndefinedOrNull(value)) { - switch (type) { - case ValueType.STRING: - return ''; - case ValueType.INTEGER: - case ValueType.DOUBLE: - return 0; - case ValueType.BOOLEAN: - return false; - case ValueType.JSON: - return {}; - } - } else { - return value; - } - } - - private computeAttributes(attributes: ScadaObjectAttribute[], value: any): {[attr: string]: any} { - const res: {[attr: string]: any} = {}; - for (const attribute of attributes) { - const attr = attribute.name; - res[attr] = this.computeValue(attribute.value, value); - } - return res; - } - - private setElementAttributes(elementState: ScadaObjectElementState, element: Element, attrs: {[attr: string]: any}, animate?: number) { - if (isDefinedAndNotNull(animate)) { - this.animation(elementState, element, animate).attr(attrs); - } else { - element.attr(attrs); - } - } - - private setElementText(element: Element, text: string) { - let textElement: Text; - if (element.type === 'text') { - const children = element.children(); - if (children.length && children[0].type === 'tspan') { - textElement = children[0] as Text; - } else { - textElement = element as Text; - } - } else if (element.type === 'tspan') { - textElement = element as Text; - } - if (textElement) { - textElement.text(text); - } - } - - private setElementFont(element: Element, font: Font, color: string) { - if (element.type === 'text') { - const textElement = element as Text; - if (font) { - textElement.font({ - family: font.family, - size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ? - font.size + font.sizeUnit : null, - weight: font.weight, - style: font.style - }); - } - if (color) { - textElement.fill(color); - } - } - } - - private animation(elementState: ScadaObjectElementState, element: Element, duration: number): Runner { - if (!elementState.animationTimeline) { - elementState.animationTimeline = new Timeline(); - } - element.timeline(elementState.animationTimeline); - return element.animate(duration, 0, 'now'); - } - - private computeValue(objectValue: ScadaObjectValue, value: any): any { - if (objectValue) { - switch (objectValue.type) { - case 'input': - return value; - case 'constant': - return objectValue.constantValue; - case 'property': - const property = this.getProperty(objectValue.propertyId); - if (property.type === 'color-settings') { - const colorProcessor: ColorProcessor = objectValue.computedPropertyValue; - colorProcessor.update(value); - return colorProcessor.color; - } else { - return objectValue.computedPropertyValue; - } - case 'function': - try { - return objectValue.valueConverter(value); - } catch (_e) { - return value; - } - case 'valueFormat': - let units = ''; - let decimals = 0; - if (objectValue.units) { - units = this.computeValue(objectValue.units, value); - } - if (objectValue.decimals) { - decimals = this.computeValue(objectValue.decimals, value); - } - return formatValue(value, decimals, units, false); - } - } else { - return ''; - } - } - - private getProperty(id: string): ScadaObjectProperty { - return this.metadata.properties.find(p => p.id === id); - } - - private getPropertyValue(id: string): any { - const property = this.getProperty(id); - if (property) { - const value = this.settings[id]; - if (isDefinedAndNotNull(value)) { - if (property.type === 'color-settings') { - return ColorProcessor.fromSettings(value); - } - return value; - } else { - switch (property.type) { - case 'string': - return ''; - case 'number': - return 0; - case 'color': - return '#000'; - case 'color-settings': - return constantColor('#000'); - } - } - } else { - return ''; - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html similarity index 87% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html index 972a5c532c..6d4345825e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html @@ -21,7 +21,7 @@
widgets.slider.behavior
{{ behaviour.name }}
- - + formControlName="{{ behaviour.id }}"> + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts similarity index 88% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts index 3e0bd7409d..32cec1be22 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts @@ -31,10 +31,10 @@ import { AppState } from '@core/core.state'; import { defaultScadaObjectSettings, parseScadaObjectMetadataFromContent, - ScadaObjectBehaviorType, - ScadaObjectMetadata, - ScadaObjectSettings -} from '@home/components/widget/lib/scada/scada.models'; + IotSvgBehaviorType, + IotSvgMetadata, + IotSvgObjectSettings +} from '@home/components/widget/lib/svg/iot-svg.models'; import { HttpClient } from '@angular/common/http'; import { ValueType } from '@shared/models/constants'; import { IAliasController } from '@core/api/widget-api.models'; @@ -43,35 +43,36 @@ import { isDefinedAndNotNull } from '@core/utils'; import { ScadaPropertyRow, toPropertyRows -} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.models'; +} from '@home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models'; import { merge, Observable, Subscription } from 'rxjs'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; @Component({ - selector: 'tb-scada-object-settings', - templateUrl: './scada-object-settings.component.html', + selector: 'tb-iot-svg-object-settings', + templateUrl: './iot-svg-object-settings.component.html', styleUrls: ['./../../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaObjectSettingsComponent), + useExisting: forwardRef(() => IotSvgObjectSettingsComponent), multi: true }, { provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaObjectSettingsComponent), + useExisting: forwardRef(() => IotSvgObjectSettingsComponent), multi: true } ] }) -export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { +export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { - ScadaObjectBehaviorType = ScadaObjectBehaviorType; + IotSvgBehaviorType = IotSvgBehaviorType; @Input() disabled: boolean; @Input() - svgPath = '/assets/widget/scada/drawing.svg'; + svgPath = 'drawing.svg'; @Input() aliasController: IAliasController; @@ -79,10 +80,13 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV @Input() targetDevice: TargetDevice; + @Input() + callbacks: WidgetActionCallbacks; + @Input() widgetType: widgetType; - private modelValue: ScadaObjectSettings; + private modelValue: IotSvgObjectSettings; private propagateChange = null; @@ -91,7 +95,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV public scadaObjectSettingsFormGroup: UntypedFormGroup; - metadata: ScadaObjectMetadata; + metadata: IotSvgMetadata; propertyRows: ScadaPropertyRow[]; constructor(protected store: Store, @@ -136,7 +140,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV } } - writeValue(value: ScadaObjectSettings): void { + writeValue(value: IotSvgObjectSettings): void { this.modelValue = value || {}; this.setupValue(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts similarity index 82% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts index 07009a98cf..12231d2b1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts @@ -14,16 +14,16 @@ /// limitations under the License. /// -import { ScadaObjectProperty } from '@home/components/widget/lib/scada/scada.models'; +import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models'; export interface ScadaPropertyRow { label: string; - properties: ScadaObjectProperty[]; - switch?: ScadaObjectProperty; + properties: IotSvgProperty[]; + switch?: IotSvgProperty; rowClass?: string; } -export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => { +export const toPropertyRows = (properties: IotSvgProperty[]): ScadaPropertyRow[] => { const result: ScadaPropertyRow[] = []; for (const property of properties) { let propertyRow = result.find(r => r.label === property.name); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 152cf25b35..5f2229cba5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -150,8 +150,8 @@ import { } from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component'; import { - ScadaObjectSettingsComponent -} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.component'; + IotSvgObjectSettingsComponent +} from '@home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component'; @NgModule({ declarations: [ @@ -207,7 +207,7 @@ import { TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, - ScadaObjectSettingsComponent, + IotSvgObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -269,7 +269,7 @@ import { TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, - ScadaObjectSettingsComponent, + IotSvgObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html similarity index 57% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html index f811e7f889..b2f4ce1815 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html @@ -15,5 +15,12 @@ limitations under the License. --> -
+
+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss similarity index 54% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss index 5012fe3a7c..b109bac863 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss @@ -13,10 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.tb-scada-shape { + +.tb-iot-svg-panel { width: 100%; height: 100%; + position: relative; display: flex; - align-items: center; - justify-content: center; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-iot-svg-overlay) { + z-index: 1; + } + .tb-iot-svg-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-iot-svg-content { + flex: 1; + min-width: 0; + min-height: 0; + .tb-iot-svg-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts similarity index 51% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts index 25451dcf15..c7b5e87723 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts @@ -28,28 +28,35 @@ import { import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; -import { SVG, Svg } from '@svgdotjs/svg.js'; import { HttpClient } from '@angular/common/http'; -import { ScadaObject, ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models'; +import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models'; import { ResizeObserver } from '@juggle/resize-observer'; -import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models'; +import { + iotSvgWidgetDefaultSettings, + IotSvgWidgetSettings +} from '@home/components/widget/lib/svg/iot-svg-widget.models'; +import { Observable } from 'rxjs'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-scada-test-widget', - templateUrl: './scada-test-widget.component.html', - styleUrls: ['../action/action-widget.scss', './scada-test-widget.component.scss'], + selector: 'tb-iot-svg-widget', + templateUrl: './iot-svg-widget.component.html', + styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'], encapsulation: ViewEncapsulation.None }) -export class ScadaTestWidgetComponent extends +export class IotSvgWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { - @ViewChild('scadaShape', {static: false}) - scadaShape: ElementRef; + @ViewChild('iotSvgShape', {static: false}) + iotSvgShape: ElementRef; + + private settings: IotSvgWidgetSettings; - private settings: ScadaTestWidgetSettings; + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + iotSvgObject: IotSvgObject; private autoScale = true; - private scadaObject: ScadaObject; private shapeResize$: ResizeObserver; @@ -63,18 +70,25 @@ export class ScadaTestWidgetComponent extends ngOnInit(): void { super.ngOnInit(); - this.settings = {...this.ctx.settings}; - this.scadaObject = new ScadaObject(this.ctx, '/assets/widget/scada/drawing.svg', this.settings.scadaObject); - this.scadaObject.init().subscribe(); + this.settings = {...iotSvgWidgetDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + this.iotSvgObject = new IotSvgObject(this.ctx, this.settings.iotSvg, this.settings.iotSvgObject); + this.iotSvgObject.onError((error) => { + this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); + }); + this.iotSvgObject.init().subscribe(); } ngAfterViewInit(): void { - this.scadaObject.addTo(this.scadaShape.nativeElement); + this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); if (this.autoScale) { this.shapeResize$ = new ResizeObserver(() => { this.onResize(); }); - this.shapeResize$.observe(this.scadaShape.nativeElement); + this.shapeResize$.observe(this.iotSvgShape.nativeElement); this.onResize(); } super.ngAfterViewInit(); @@ -84,19 +98,22 @@ export class ScadaTestWidgetComponent extends if (this.shapeResize$) { this.shapeResize$.disconnect(); } - if (this.scadaObject) { - this.scadaObject.destroy(); + if (this.iotSvgObject) { + this.iotSvgObject.destroy(); } super.ngOnDestroy(); } public onInit() { super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); } private onResize() { - const shapeWidth = this.scadaShape.nativeElement.getBoundingClientRect().width; - const shapeHeight = this.scadaShape.nativeElement.getBoundingClientRect().height; - this.scadaObject.setSize(shapeWidth, shapeHeight); + const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width; + const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height; + this.iotSvgObject.setSize(shapeWidth, shapeHeight); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts similarity index 50% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts index a74d81e3d5..e4992ae62f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts @@ -14,8 +14,25 @@ /// limitations under the License. /// -import { ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models'; +import { IotSvgObjectSettings } from '@home/components/widget/lib/svg/iot-svg.models'; +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; -export interface ScadaTestWidgetSettings { - scadaObject: ScadaObjectSettings; +export interface IotSvgWidgetSettings { + iotSvg: string; + iotSvgObject: IotSvgObjectSettings; + background: BackgroundSettings; } + +export const iotSvgWidgetDefaultSettings: IotSvgWidgetSettings = { + iotSvg: '/assets/widget/svg/drawing.svg', + iotSvgObject: {}, + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts new file mode 100644 index 0000000000..d0bd66dcbf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -0,0 +1,634 @@ +/// +/// Copyright ยฉ 2016-2024 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 { ValueType } from '@shared/models/constants'; +import { Box, Element, Runner, Svg, SVG, Text } from '@svgdotjs/svg.js'; +import { + DataToValueType, + GetValueAction, + GetValueSettings, + SetValueAction, + SetValueSettings, + ValueToDataType +} from '@shared/models/action-widget-settings.models'; +import { formatValue, isDefinedAndNotNull, isUndefinedOrNull, mergeDeep, parseFunction } from '@core/utils'; +import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs'; +import { map, share } from 'rxjs/operators'; +import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { WidgetAction, WidgetActionType } from '@shared/models/widget.models'; + +export interface IotSvgApi { + formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; + text: (element: Element | Element[], text: string) => void; + font: (element: Element | Element[], font: Font, color: string) => void; + animate: (element: Element, duration: number) => Runner; + disable: (element: Element | Element[]) => void; + enable: (element: Element | Element[]) => void; + callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial>) => void; + setValue: (valueId: string, value: any) => void; +} + +export interface IotSvgContext { + api: IotSvgApi; + tags: {[id: string]: Element[]}; + values: {[id: string]: any}; + properties: {[id: string]: any}; +} + +export type IotSvgStateRenderFunction = (svg: Svg, ctx: IotSvgContext) => void; + +export type IotSvgTagStateRenderFunction = (element: Element, ctx: IotSvgContext) => void; + +export type IotSvgActionTrigger = 'click'; + +export type IotSvgActionFunction = (event: Event, ctx: IotSvgContext) => void; +export interface IotSvgAction { + actionFunction?: string; + action?: IotSvgActionFunction; +} + +export interface IotSvgTag { + tag: string; + stateRenderFunction?: string; + stateRender?: IotSvgTagStateRenderFunction; + actions?: {[trigger: string]: IotSvgAction}; +} + +export enum IotSvgBehaviorType { + value = 'value', + action = 'action', + widgetAction = 'widgetAction' +} + +export interface IotSvgBehaviorBase { + id: string; + name: string; + type: IotSvgBehaviorType; +} + +export interface IotSvgBehaviorValue extends IotSvgBehaviorBase { + valueType: ValueType; + defaultValue: any; + valueId: string; +} + +export interface IotSvgBehaviorAction extends IotSvgBehaviorBase { + valueToDataType: ValueToDataType; + constantValue: any; + valueToDataFunction: string; +} + +export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction; + +export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; + +export interface IotSvgPropertyBase { + id: string; + name: string; + type: IotSvgPropertyType; + default: any; + required?: boolean; + subLabel?: string; + divider?: boolean; + fieldSuffix?: string; + disableOnProperty?: string; + rowClass?: string; + fieldClass?: string; +} + +export interface IotSvgNumberProperty extends IotSvgPropertyBase { + min?: number; + max?: number; + step?: number; +} + +export type IotSvgProperty = IotSvgPropertyBase & IotSvgNumberProperty; + +export type ScadaObjectElementActionTrigger = 'click'; + +export type ScadaObjectActionType = 'updateValue'; +export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant'; + +export interface ScadaObjectElementAction { + trigger: ScadaObjectElementActionTrigger; + enabledTriggerValues?: string[]; + enabledConditionFunction?: string; + enabledCondition?: (values: {[key: string]: any}) => boolean; + actionType: ScadaObjectActionType; + updateValueId?: string; + updateValueType?: ScadaObjectActionUpdateValueType; + updateValueConstant?: any; + updateValueInc?: number; +} + +export interface IotSvgMetadata { + title: string; + stateRenderFunction?: string; + stateRender?: IotSvgStateRenderFunction; + tags: IotSvgTag[]; + behavior: IotSvgBehavior[]; + properties: IotSvgProperty[]; +} + +export const emptyMetadata: IotSvgMetadata = { + title: '', + tags: [], + behavior: [], + properties: [] +}; + + +export const parseScadaObjectMetadataFromContent = (svgContent: string): IotSvgMetadata => { + try { + const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + return parseScadaObjectMetadataFromDom(svgDoc); + } catch (_e) { + return emptyMetadata; + } +}; + +const parseScadaObjectMetadataFromDom = (svgDoc: Document): IotSvgMetadata => { + try { + const elements = svgDoc.getElementsByTagName('tb:metadata'); + if (elements.length) { + return JSON.parse(elements[0].textContent); + } else { + return emptyMetadata; + } + } catch (_e) { + console.error(_e); + return emptyMetadata; + } +}; + +const defaultGetValueSettings = (get: IotSvgBehaviorValue): GetValueSettings => ({ + action: GetValueAction.DO_NOTHING, + defaultValue: get.defaultValue, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }); + +const defaultSetValueSettings = (set: IotSvgBehaviorAction): SetValueSettings => ({ + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SERVER_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: set.valueToDataType, + constantValue: set.constantValue, + valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction : + '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' + } +}); + +const defaultWidgetActionSettings = (widgetAction: IotSvgBehavior): WidgetAction => ({ + type: WidgetActionType.updateDashboardState, + targetDashboardStateId: null, + openRightLayout: false, + setEntityId: true, + stateEntityParamName: null +}); + +export const defaultScadaObjectSettings = (metadata: IotSvgMetadata): IotSvgObjectSettings => { + const settings: IotSvgObjectSettings = {}; + for (const behavior of metadata.behavior) { + if (behavior.type === IotSvgBehaviorType.value) { + settings[behavior.id] = defaultGetValueSettings(behavior as IotSvgBehaviorValue); + } else if (behavior.type === IotSvgBehaviorType.action) { + settings[behavior.id] = defaultSetValueSettings(behavior as IotSvgBehaviorAction); + } else if (behavior.type === IotSvgBehaviorType.widgetAction) { + settings[behavior.id] = defaultWidgetActionSettings(behavior); + } + } + for (const property of metadata.properties) { + settings[property.id] = property.default; + } + return settings; +}; + +export type IotSvgObjectSettings = {[id: string]: any}; + +const parseError = (ctx: WidgetContext, err: any): string => + ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; + +export class IotSvgObject { + + private metadata: IotSvgMetadata; + private settings: IotSvgObjectSettings; + private context: IotSvgContext; + + private rootElement: HTMLElement; + private svgShape: Svg; + private box: Box; + private targetWidth: number; + private targetHeight: number; + + private loadingSubject = new BehaviorSubject(false); + private valueGetters: ValueGetter[] = []; + private valueActions: ValueAction[] = []; + private valueSetters: {[behaviorId: string]: ValueSetter} = {}; + + private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; + + loading$ = this.loadingSubject.asObservable().pipe(share()); + + private _onError: (error: string) => void = () => {}; + + constructor(private ctx: WidgetContext, + private svgPath: string, + private inputSettings: IotSvgObjectSettings) {} + + public init(): Observable { + return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( + map((inputSvgContent) => { + const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); + this.metadata = parseScadaObjectMetadataFromDom(doc); + const defaults = defaultScadaObjectSettings(this.metadata); + this.settings = mergeDeep({}, defaults, this.inputSettings || {}); + this.prepareMetadata(); + this.prepareSvgShape(doc); + this.initialize(); + }) + ); + } + + public onError(onError: (error: string) => void) { + this._onError = onError; + } + + public addTo(element: HTMLElement) { + this.rootElement = element; + if (this.svgShape) { + this.svgShape.addTo(element); + } + } + + public destroy() { + for (const stateValueId of Object.keys(this.stateValueSubjects)) { + this.stateValueSubjects[stateValueId].complete(); + this.stateValueSubjects[stateValueId].unsubscribe(); + } + this.valueActions.forEach(v => v.destroy()); + this.loadingSubject.complete(); + this.loadingSubject.unsubscribe(); + } + + public setSize(targetWidth: number, targetHeight: number) { + this.targetWidth = targetWidth; + this.targetHeight = targetHeight; + if (this.svgShape) { + this.resize(); + } + } + + private prepareMetadata() { + this.metadata.stateRender = parseFunction(this.metadata.stateRenderFunction, ['svg', 'ctx']) || (() => {}); + for (const tag of this.metadata.tags) { + tag.stateRender = parseFunction(tag.stateRenderFunction, ['element', 'ctx']) || (() => {}); + if (tag.actions) { + for (const trigger of Object.keys(tag.actions)) { + const action = tag.actions[trigger]; + action.action = parseFunction(action.actionFunction, ['event', 'ctx']) || (() => {}); + } + } + } + } + + private prepareSvgShape(doc: XMLDocument) { + const elements = doc.getElementsByTagName('tb:metadata'); + for (let i=0;i { + element.attr('cursor', 'pointer'); + element.on(trigger, (event) => { + action.action(event, this.context); + }); + }); + } + } + } + for (const behavior of this.metadata.behavior) { + if (behavior.type === IotSvgBehaviorType.value) { + const getBehavior = behavior as IotSvgBehaviorValue; + let getValueSettings: GetValueSettings = this.settings[getBehavior.id]; + getValueSettings = {...getValueSettings, actionLabel: getBehavior.name}; + const stateValueSubject = new BehaviorSubject(getValueSettings.defaultValue); + this.stateValueSubjects[getBehavior.valueId] = stateValueSubject; + this.context.values[getBehavior.valueId] = getValueSettings.defaultValue; + stateValueSubject.subscribe((value) => { + this.onStateValueChanged(getBehavior.valueId, value); + }); + const valueGetter = + ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, { + next: (val) => {this.onValue(getBehavior.id, val);}, + error: (err) => { + const message = parseError(this.ctx, err); + this._onError(message); + } + }); + this.valueGetters.push(valueGetter); + this.valueActions.push(valueGetter); + } else if (behavior.type === IotSvgBehaviorType.action) { + const setBehavior = behavior as IotSvgBehaviorAction; + let setValueSettings: SetValueSettings = this.settings[setBehavior.id]; + setValueSettings = {...setValueSettings, actionLabel: setBehavior.name}; + const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings); + this.valueSetters[setBehavior.id] = valueSetter; + this.valueActions.push(valueSetter); + } else if (behavior.type === IotSvgBehaviorType.widgetAction) { + // TODO: + } + } + this.renderState(); + if (this.valueGetters.length) { + const getValueObservables: Array> = []; + this.valueGetters.forEach(valueGetter => { + getValueObservables.push(valueGetter.getValue()); + }); + this.loadingSubject.next(true); + forkJoin(getValueObservables).subscribe( + { + next: () => { + this.loadingSubject.next(false); + }, + error: () => { + this.loadingSubject.next(false); + } + } + ); + } + } + + private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial>) { + const behavior = this.metadata.behavior.find(b => b.id === behaviorId); + if (behavior) { + if (behavior.type === IotSvgBehaviorType.action) { + const valueSetter = this.valueSetters[behaviorId]; + if (valueSetter) { + this.loadingSubject.next(true); + valueSetter.setValue(value).subscribe( + { + next: () => { + if (observer?.next) { + observer.next(); + } + this.loadingSubject.next(false); + }, + error: (err) => { + this.loadingSubject.next(false); + if (observer?.error) { + observer.error(err); + } + const message = parseError(this.ctx, err); + this._onError(message); + } + } + ); + } + } else if (behavior.type === IotSvgBehaviorType.widgetAction) { + const widgetAction: WidgetAction = this.settings[behavior.id]; + this.ctx.actionsApi.onWidgetAction(event, widgetAction); + } + } + } + + private resize() { + let scale: number; + if (this.targetWidth < this.targetHeight) { + scale = this.targetWidth / this.box.width; + } else { + scale = this.targetHeight / this.box.height; + } + this.svgShape.node.style.transform = `scale(${scale})`; + } + + private onValue(id: string, value: any) { + const valueBehavior = this.metadata.behavior.find(b => b.id === id) as IotSvgBehaviorValue; + value = this.normalizeValue(value, valueBehavior.valueType); + const valueId = valueBehavior.valueId; + this.setValue(valueId, value); + } + + private setValue(valueId: string, value: any) { + const stateValueSubject = this.stateValueSubjects[valueId]; + if (stateValueSubject && stateValueSubject.value !== value) { + stateValueSubject.next(value); + } + } + + private onStateValueChanged(id: string, value: any) { + if (this.context.values[id] !== value) { + this.context.values[id] = value; + this.renderState(); + } + } + + private renderState(): void { + this.metadata.stateRender(this.svgShape, this.context); + for (const tag of this.metadata.tags) { + const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); + elements.forEach(element => { + tag.stateRender(element, this.context); + }); + } + } + + private normalizeValue(value: any, type: ValueType): any { + if (isUndefinedOrNull(value)) { + switch (type) { + case ValueType.STRING: + return ''; + case ValueType.INTEGER: + case ValueType.DOUBLE: + return 0; + case ValueType.BOOLEAN: + return false; + case ValueType.JSON: + return {}; + } + } else { + return value; + } + } + + private setElementText(e: Element | Element[], text: string) { + this.elements(e).forEach(element => { + let textElement: Text; + if (element.type === 'text') { + const children = element.children(); + if (children.length && children[0].type === 'tspan') { + textElement = children[0] as Text; + } else { + textElement = element as Text; + } + } else if (element.type === 'tspan') { + textElement = element as Text; + } + if (textElement) { + textElement.text(text); + } + }); + } + + private setElementFont(e: Element | Element[], font: Font, color: string) { + this.elements(e).forEach(element => { + if (element.type === 'text') { + const textElement = element as Text; + if (font) { + textElement.font({ + family: font.family, + size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ? + font.size + font.sizeUnit : null, + weight: font.weight, + style: font.style + }); + } + if (color) { + textElement.fill(color); + } + } + }); + } + + private animate(element: Element, duration: number): Runner { + element.timeline().finish(); + return element.animate(duration, 0, 'now'); + } + + private disableElement(e: Element | Element[]) { + this.elements(e).forEach(element => { + element.attr({'pointer-events': 'none'}); + }); + } + + private enableElement(e: Element | Element[]) { + this.elements(e).forEach(element => { + element.attr({'pointer-events': null}); + }); + } + + private elements(element: Element | Element[]): Element[] { + return Array.isArray(element) ? element : [element]; + } + + private getProperty(id: string): IotSvgProperty { + return this.metadata.properties.find(p => p.id === id); + } + + private getPropertyValue(id: string): any { + const property = this.getProperty(id); + if (property) { + const value = this.settings[id]; + if (isDefinedAndNotNull(value)) { + if (property.type === 'color-settings') { + return ColorProcessor.fromSettings(value); + } + return value; + } else { + switch (property.type) { + case 'string': + return ''; + case 'number': + return 0; + case 'color': + return '#000'; + case 'color-settings': + return constantColor('#000'); + } + } + } else { + return ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index f062494763..ae9034d1cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -92,7 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component'; import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component'; import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component'; -import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scada-test-widget.component'; +import { IotSvgWidgetComponent } from '@home/components/widget/lib/svg/iot-svg-widget.component'; @NgModule({ declarations: @@ -152,7 +152,7 @@ import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scad BarChartWidgetComponent, PolarAreaWidgetComponent, RadarChartWidgetComponent, - ScadaTestWidgetComponent + IotSvgWidgetComponent ], imports: [ CommonModule, @@ -215,7 +215,7 @@ import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scad BarChartWidgetComponent, PolarAreaWidgetComponent, RadarChartWidgetComponent, - ScadaTestWidgetComponent + IotSvgWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index a79fa89242..b8f9be3f56 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -39,7 +39,7 @@ import { } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { - Widget, + Widget, WidgetAction, WidgetActionDescriptor, widgetActionSources, WidgetActionType, @@ -57,7 +57,7 @@ import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { - deepClone, + deepClone, guid, insertVariable, isDefined, isNotEmptyStr, @@ -254,6 +254,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI actionDescriptorsBySourceId, getActionDescriptors: this.getActionDescriptors.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this), + onWidgetAction: this.onWidgetAction.bind(this), elementClick: this.elementClick.bind(this), cardClick: this.cardClick.bind(this), click: this.click.bind(this), @@ -1039,7 +1040,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI return result; } - private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor, + private handleWidgetAction($event: Event, descriptor: WidgetAction, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void { const type = descriptor.type; const targetEntityParamName = descriptor.stateEntityParamName; @@ -1112,7 +1113,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const customHtml = descriptor.customHtml; const customCss = descriptor.customCss; const customResources = descriptor.customResources; - const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`; + const actionNamespace = `custom-action-pretty-${guid()}`; let htmlTemplate = ''; if (isDefined(customHtml) && customHtml.length > 0) { htmlTemplate = customHtml; @@ -1420,13 +1421,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]); if (targetElement.length && targetElement[0].id) { - $event.stopPropagation(); const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id); - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + this.onWidgetAction($event, descriptor); } } } @@ -1442,18 +1438,23 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private onClick($event: Event, sourceId: string) { const descriptors = this.getActionDescriptors(sourceId); if (descriptors.length) { + this.onWidgetAction($event, descriptors[0]); + } + } + + private onWidgetAction($event: Event, action: WidgetAction) { + if ($event) { $event.stopPropagation(); - const descriptor = descriptors[0]; - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); } + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, action, entityId, entityName, null, entityLabel); } private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, - actionDescriptor: WidgetActionDescriptor): Observable { + actionDescriptor: WidgetAction): Observable { const resourceTasks: Observable[] = []; const modulesTasks: Observable[] = []; diff --git a/ui-ngx/src/assets/widget/scada/drawing.svg b/ui-ngx/src/assets/widget/svg/drawing.svg similarity index 54% rename from ui-ngx/src/assets/widget/scada/drawing.svg rename to ui-ngx/src/assets/widget/svg/drawing.svg index 61e8d3f1ca..91bfb8b47d 100644 --- a/ui-ngx/src/assets/widget/scada/drawing.svg +++ b/ui-ngx/src/assets/widget/svg/drawing.svg @@ -1,341 +1,115 @@ - { - "title": "My first SCADA Object", - "stateValues": { - "level": { - "initialValue": 0 - }, - "disabled": { - "initialValue": false + { + "title": "My first IoT SVG Object", + "stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}", + "tags": [ + { + "tag": "level", + "stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});" }, - "on": { - "initialValue": false, - "callBehavior": [ - { - "behaviorId": "onUpdateState", - "conditionFunction": "return value === true;" - }, - { - "behaviorId": "offUpdateState", - "conditionFunction": "return value === false;" - } - ] - } - }, - "actions": { - "onButton": { - "trigger": "click", - "actionType": "updateValue", - "updateValueId": "on", - "updateValueType": "constant", - "updateValueConstant": true + { + "tag": "levelTitle", + "stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}" }, - "offButton": { - "trigger": "click", - "actionType": "updateValue", - "updateValueId": "on", - "updateValueType": "constant", - "updateValueConstant": false - } - }, - "states": { - "initialState": { - "initial": true, - "state": [ - { - "tag": "levelTitle", - "show": { - "type": "property", - "propertyId": "showLevelTitle" - }, - "text": { - "content": { - "type": "property", - "propertyId": "levelTitle" - }, - "font": { - "type": "property", - "propertyId": "levelTitleFont" - }, - "color": { - "type": "property", - "propertyId": "levelTitleColor" - } - } - }, - { - "tag": "level", - "inputValue": "level", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "levelBackground" - } - } - ] - }, - { - "tag": "levelValueBackground", - "show": { - "type": "property", - "propertyId": "showValue" - } - }, - { - "tag": "levelValue", - "show": { - "type": "property", - "propertyId": "showValue" - }, - "text": { - "font": { - "type": "property", - "propertyId": "valueFont" - }, - "color": { - "type": "property", - "propertyId": "valueColor" - } - } - }, - { - "tag": "minLevel", - "show": { - "type": "property", - "propertyId": "showMinMaxLevel" - }, - "text": { - "content": { - "type": "property", - "propertyId": "minLevel" - }, - "font": { - "type": "property", - "propertyId": "minMaxLevelFont" - }, - "color": { - "type": "property", - "propertyId": "minMaxLevelColor" - } - } - }, - { - "tag": "maxLevel", - "show": { - "type": "property", - "propertyId": "showMinMaxLevel" - }, - "text": { - "content": { - "type": "property", - "propertyId": "maxLevel" - }, - "font": { - "type": "property", - "propertyId": "minMaxLevelFont" - }, - "color": { - "type": "property", - "propertyId": "minMaxLevelColor" - } - } - } - ] + { + "tag": "levelValue", + "stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'levelValueClick');" + } + } }, - "updateLevelState": { - "triggerValues": ["level"], - "state": [ - { - "tag": "level", - "inputValue": "level", - "animate": 200, - "attributes": [ - { - "name": "height", - "value": { - "type": "function", - "valueConvertFunction": "var level = (value - ${minLevel}) / (${maxLevel} - ${minLevel});\nlevel = Math.max(0, Math.min(1, level));\nreturn level*80;" - } - } - ] - }, - { - "tag": "levelValue", - "inputValue": "level", - "text": { - "content": { - "type": "valueFormat", - "units": { - "type": "property", - "propertyId": "valueUnits" - }, - "decimals": { - "type": "property", - "propertyId": "valueDecimals" - } - } - } - } - ] + { + "tag": "fan", + "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n" }, - "updateLevelBackgroundState": { - "triggerValues": ["disabled", "level"], - "conditionFunction": "return values.disabled === false;", - "state": [ - { - "tag": "level", - "inputValue": "level", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "levelBackground" - } - } - ] - } - ] + { + "tag": "onButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'onUpdateState');" + } + } }, - "disabledState": { - "triggerValues": ["disabled"], - "conditionFunction": "return values.disabled === true;", - "state": [ - { - "tag": "level", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "disabledLevelBackground" - } - } - ] - } - ] + { + "tag": "offButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'offUpdateState');" + } + } }, - "onState": { - "triggerValues": ["on"], - "conditionFunction": "return values.on === true;", - "state": [ - { - "tag": "fan", - "addClass": "fan-rotate" - }, - { - "tag": "onButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#777" - } - } - ] - }, - { - "tag": "offButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#ed121f" - } - } - ] - } - ] + { + "tag": "levelUpButton", + "actions": { + "click": { + "actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" + } + } }, - "offState": { - "triggerValues": ["on"], - "conditionFunction": "return values.on === false;", - "state": [ - { - "tag": "fan", - "removeClass": "fan-rotate" - }, - { - "tag": "onButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#12ed19" - } - } - ] - }, - { - "tag": "offButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#777" - } - } - ] - } - ] + { + "tag": "levelDownButton", + "actions": { + "click": { + "actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" + } + } } - }, + ], "behavior": [ { "id": "levelState", "name": "Level", - "type": "getValue", + "type": "value", "valueType": "DOUBLE", "defaultValue": 0, - "onUpdate": [ - { - "updateValue": "level" - } - ] + "valueId": "level" }, { "id": "disabledState", "name": "Disabled state", - "type": "getValue", + "type": "value", "valueType": "BOOLEAN", "defaultValue": false, - "onUpdate": [ - { - "updateValue": "disabled" - } - ] + "valueId": "disabled" }, { "id": "onState", "name": "On/Off state", - "type": "getValue", + "type": "value", "valueType": "BOOLEAN", "defaultValue": false, - "onUpdate": [ - { - "updateValue": "on" - } - ] + "valueId": "on" }, { "id": "onUpdateState", "name": "Power On", - "type": "setValue", + "type": "action", + "valueType": "BOOLEAN", "valueToDataType": "CONSTANT", "constantValue": true }, { "id": "offUpdateState", "name": "Power Off", - "type": "setValue", + "type": "action", + "valueType": "BOOLEAN", "valueToDataType": "CONSTANT", "constantValue": false + }, + { + "id": "levelUpdateState", + "name": "Update level", + "type": "action", + "valueType": "DOUBLE", + "valueToDataType": "VALUE", + "constantValue": 0 + }, + { + "id": "levelValueClick", + "name": "On level value click", + "type": "widgetAction" } ], "properties": [ @@ -489,7 +263,7 @@ "default": "#ccc" } ] - }</tb:metadata> + } From c4ff43a840f666ae10740bcb98fb331ab778b4cc Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 7 May 2024 20:02:06 +0300 Subject: [PATCH 007/331] UI: Minor fix. --- .../modules/home/components/widget/lib/svg/iot-svg.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index d0bd66dcbf..9b16e56649 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -624,7 +624,7 @@ export class IotSvgObject { case 'color': return '#000'; case 'color-settings': - return constantColor('#000'); + return ColorProcessor.fromSettings(constantColor('#000')); } } } else { From 0beeb99b99df9cb6c809c66c0564194a830c61ba Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 8 May 2024 17:13:25 +0300 Subject: [PATCH 008/331] UI: Refactoring and improvements --- .../json/system/widget_bundles/cards.json | 4 +- .../json/system/widget_types/iot_svg.json | 26 +++++++++++ .../action-button-basic-config.component.html | 10 ++-- ...command-button-basic-config.component.html | 6 +-- .../power-button-basic-config.component.html | 16 +++---- .../toggle-button-basic-config.component.html | 16 +++---- .../status-widget-basic-config.component.html | 12 ++--- .../single-switch-basic-config.component.html | 16 +++---- .../rpc/slider-basic-config.component.html | 8 ++-- ...tion-button-widget-settings.component.html | 8 ++-- ...mand-button-widget-settings.component.html | 6 +-- ...ower-button-widget-settings.component.html | 16 +++---- ...ggle-button-widget-settings.component.html | 16 +++---- ...value-action-settings-panel.component.html | 4 +- ...t-value-action-settings-panel.component.ts | 4 +- .../get-value-action-settings.component.ts | 12 +++-- ...value-action-settings-panel.component.html | 2 +- ...idget-action-settings-panel.component.html | 2 +- .../iot-svg-object-settings.component.html | 29 ++++++------ .../svg/iot-svg-object-settings.component.ts | 44 +++++++++--------- .../svg/iot-svg-object-settings.models.ts | 6 +-- ...ngle-switch-widget-settings.component.html | 16 +++---- .../slider-widget-settings.component.html | 8 ++-- .../status-widget-settings.component.html | 12 ++--- .../widget/lib/svg/iot-svg.models.ts | 46 +++++++++---------- .../components/value-input.component.html | 4 +- .../components/value-input.component.ts | 12 ++++- .../app/shared/pipe/custom-translate.pipe.ts | 33 +++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 4 ++ .../assets/locale/locale.constant-en_US.json | 3 ++ ui-ngx/src/assets/widget/svg/drawing.svg | 15 ++++-- 31 files changed, 252 insertions(+), 164 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/iot_svg.json create mode 100644 ui-ngx/src/app/shared/pipe/custom-translate.pipe.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 39996839f1..43f1ab4886 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -2,10 +2,9 @@ "widgetsBundle": { "alias": "cards", "title": "Cards", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC/VBMVEUAAAAuTILg4ODf398dMkk0SWLg4ODg4OAyP1UrQF8sRH2/v8fDx8dFbHsDCBhKWGvDxsYaNET///8AAADg4OD4+Pjn5+fj4+P6+vrv7+/q8voKCgre3d3R2eft7e3TzMjP1+UdHR3My8vQzs7r6+vz8/PQ0NCKiorS0tPb29zm8Pnp6enQzMrX19j19fXNzc3m5eXW1dacnJzT1NnT2+nJycrx8fHf6vTMztHh7PXP1ePTzswbGxwWFhYsLC3Y4u7Wzcjj7ffT1NTS2OHl4NvZ1tXPz9QhISHVzst3d3jZ2drX1NNKSkrP1N6Xl5fn495tbW3d3uHW0tGysrLs7/HY3OG4ubnl7vbi6fLL0NhSUlLd5/PS0NCqqalXVlbp7e/Zz8rs6eXg4+XLzM0yUI3e4ubf0coSEhLc5vGsrKynp6bl6OvV2eBjY2MkJCbO0tvHxsbT1t3Q0dfBwMCSkpLj5efq5eG1tbU2MihVav/d3+Ta2Nfa09COjo7W3+vMztXl6u7Y3uakpKSEhIQ7Ozxynsy7u7ve2dbDw8QxMTLu8fPg3Nng5+7IzNJLWnHi5ekgN2rw8/ba5PDU2uSfn59AYZqamZlcXF03NzejoqHn6/Ld4+oODg7d2dqvr6/q8fbu7Oo7W5MpKSpsmMdxcXLe1dG9vb2Sf25OTk/j3Nbl1cpMcKZFRUZkjsA2VJJ8fHwiPXJGaaB/f4Dd1dQqRXy3zuWZqsmOiodRYnllZmd1hqJpeZPW2dlchblzdXZBQUEPIDjFyM9WfbVghrBRdq6Xn6tpaWkSI0Oqu9qyxN+4vMJ0gZlteIgcNGANFyagprCgs9GAk7mSmKMeKjiwt75YfalXZoBBTmWssLaGipFBOS4XJCVfcIqBhIl9bFvGvrXAtKCOk5gKGS46JBilq7RVWFwjMyuVoOllb3lsXk8lFg5kdvp3hfK1r6qyo5Gax6iFkKViiJufk4alquKTjIs6cIRXNyiv1LqKpqjE38t8jLCzq6GGvZdrr35uroH7pmE0AAAAEnRSTlMA/u8g/v6/r/4gtyC/v7+wr69GtHW3AAAd4UlEQVR42sTYb0wTZxzAceaSLfvzYrlcr60pjacFet4oLRVE0ris3SQW2u2UIuCk8bBGaRFaqIaillqnlT+rUqUoAXVDQyIbDNwSFifBOaMuJmrYoi/IQjLiC7fwwhfGZVv2e55C/+yYDBj4FcqhJN7H3z3PXU1Keu31ly6/FNeOqc5HOxXpS9ylSD9G+07Qj9ClS1+eeumTQCDw1htJ0MuvLCMXsVdfBgc69S9wU6eNTxwXd+bXcddQ30b6HPrs88+m+3gq/A380bfXLp3/ieM4feANcCyTEotZ9rKXk17HjggEU6KKOMf1eMe1aQaWJChiGJBcBwjJ8/o3k5JezSYWN+krSZd3YIhwItiROBEMAUdMMjUNYSC5duonjud5LpCURBKL3TIMOX/v65m6+/Xd+G7GdyvWVzN269bNu6M8qnopICSCfPHdiriSI61ZAx/THYz2TrTW1tZd0NZIa6dat3a6ra0HO6xLC9lxT+jAlsQijoNCSMwBkGhrd3U0F/Jutztr6SZyL3cGSdxEkhEDIFBsIPEQgeJd+Nza2lwYhOIhu8X4b20LwGsFIdpNRMpm/yeI0IGbYiSvCPlwiRBwQALHu9OtbS5uRJCRGGRb9UkKvvSpurJOp/trKtKIrmrjvt27uw4UHTL6KVXRESYzq8tMmWuNJ2vmBcn914mAIzfX4bF5OI/NJpxIDJKoWL363XXNZxrtUHkUIjITqpMIQqzKJPz+AVEasc3ffcQ8FNj8oXlzLSGvbcuorTFfPX3EX2vOXNCllYwhiQNZkWuw8Dxj5NwdvpkhiQ5QoLasKxZAuovkZvhac2Bb0YHU0xfNFYS6oi0lbYBp22zO2EyI0vpWthUd8Ov9YSbt4nwWey4UHYlwIiYLz13RMnzQFz8SISTKwE1DWjAEV51Wm03MnqjtELzOExIbiWAiGKJVGnmbb03cNiyAJDhgJAjSYG/AkEVNCBFOBIpA0vUMQEKhkC8SWFqFA4k5lmNIA5SzlBBoSgGOaHGXFs8wHO9xOC0eWySPLxGS6Fi+/HkQkhFJRKoUkUilEhOS7BQim0gRkbIUgkghRATJwrfzhkAz7r+xXQsEFqfTY2dZBSpoO4ivrRgk3vFciIQkSLVRQa7UGI0MQZIMo2aMpDEdjtFvyFQaGTN/CPRPiOk+ZFqTHDI4LD6Lz2dxVjndVqlUWihtlNptsWvrORCr1SqciIykjGqVlFRQarGEkrIKmVpKkWqKgBcZQakYsXwBEKHENB6euG8ImXy+kNPncPgsJSUON6WQS6WKbMpuC9+OjOSdXbveAcmMECu0xGsElwhZkTvWZ9s3XHpooKCndKLA2Rtur9ndU7Cvb7h2qDL8oL3y+6sHh1vae0dKS+8nQJa/CEg/vLsFSEyCizhMYwPusH8g7DenFrA9qT2OAX9PWt/QxpPqLnO7d+I2hgxvfdg93otG8p8hKvgFqxqtbfgKC1wk+X8gvwMhnmIwIAuGUGW1RVn+NmWBtVTZYxkYK8jcfGRjZn17dS+GrBlu6d01NjY6ujYi2bJl9VlwxCCF1sINAkhWdhbLsmIxwZByWBxyjZols0npAiGXL1/+3RQLLAan0wA5nVXjB6jysvBJc4V+qOGGviBcGrjYE65IO91UWdkz8jA80hMua6nsKJmYGG9ubi4uLl63ZcuZM2fPRiEnrNbCGSCaDFKsJ0kZQBjSKBOTJMkyYvXCIMcRpKpK63JV4cDidGpR6fDJUQqWoliFtQH2n1JLlcOt2PvBe++RanfQ4wv5LBzalW0dvg6oGUI/VhgHKYSUAohIJVJJRSK4wuQSuUgigWOJSq9YIKS/v/+bKpfLtQHldQEFHCiNRo8hajWlUFihLosWIOe2792rZhV2uMs7mCYZUCB8n4GCdlYxI2T2JAtcI3f6jx//pgQ5cnJAguaCJ+Jy6fMBYldQYlLMIojcbtE63Qr5OQUL39s9cHsxHu2Uybjp4AoRq7efAwhOAFnM8EQAgh05q1aBxOXSolzKTflKDCFlIJFDAME3RDnc2OV2D6yi+qOdnQyDLCRKLBZCGhuXDtIfg6RugJR6SPnRpk0Iwqpl9VdISgGQoKUKIPBvHFEBJL3eCA8VDIcvLj4Ib9BhPcVDwNGoX3II5EUjwW2IQChW3PS+xohGonADhLcrcCxvMZicmveNxvpOBJle7cFESOOSQ0Di9ZbgqnCuDUoEcVNkvV5/hXPb7SxAtBYPj0IPkCYT7Afp7w8ODnZyNlBs3Vpc3Gx/8RDswAYTqsqFIBY3z1zR6+H8bbzbbYGt2eGw4BwIos/XDF64cGGwkww2FEMvEnJnerF7ESMykTgI73HAynca4Pxt6GoyoSdh2Gl9ITiGjS3/ws6dO/cflfENoICEkBNLBTmOtt8NUAkASrw5OS6XExzTE7HB2RtQEUiuwwc3C6u1oSOEIJv2YwgaSXMQgsUuj4egtHOBpIhmuduoVLNBvC4TQFrKc1KVWm0MEvSFsMMZKKrRwk84PEHWrQyMWGAk2vz9b//284X9g4NHGY7LKjqiVMP2uwBIoPTYsUoSeYqGqomZUj2TPwfyQ4kXdi0vLI+W8vKM1FTYuUpu3kIQxm0P2izQ2GGapj/9cAQg7gPDeXn0sRsjBq3m0Hpd3tBHRribpB3Oo+m84bYoZPXcIYE9vRW1ew6LxJXteXlFxExd+fPpv0PuIAjefHPKAZKRsWqV93Fd3ZNNH+kZimXdPEl20/T6snZaVwCQqzTdHgZXaZVTf6zg9sO8NoAOIQWy1CaukRMn5rDYh/I0BHEjL6u6oLYbIDM22UhEgmuwUHhpASQjAyjTkCd1T359jCBuiqLc6mqaLhOLmTKd7hfDbZoOe4KOME3/4gzQ46Gq9RtDoXFwtdg6Wnrp9cp4SLDhhD3nP0OKuiQEsTEvFQ7TAPLc3H8SfzyTPrUmPmvhSysaSB7V3XpcB5eWAz0K8ryfpgM8yT3Q6R4a+nR0Dfx3EDp2rmrvGR3T3Q8Z2uljLo7jO0ZoOu2fkPI57VqKY8PE7BDRU8mk6q8/rE+fSRIngiH4TuJtgfWe86jO+6guH/ZdB4ozD3UbORlzG05+xXjlkCMUWoOOnemZn+p0E6HQqE531cHISI9tmC6LXyNB+wlr1pwgN/bUzArBTUomJwlyUiWAYAeWwGMwnogG7h8oh4UjSU4mY9J0utHk5JADQR7CsVZTX5P5AG4s47QuM93YBA/17sT7CHuO2j4XCAAyCQFkloQQTMEvj+rgQ6ONOIyMDLdyPV0aSoa3ISBpWa8rNWjzBzVajZHjBnS63en1R5uamsTxkIbGc9s/YFfOAdKW10fMF3IHdi0vLPbI2xEXQFwuuK5+fVz3N/tmF9NWGcZxP5jOCzWetD1t0kDaA93BtqOlKcG0IqntKFQptoq6nDqQqCnMWqzSabuosMAMsiVKqpWULAtjYkwweCFzZMLFmA6FBAhMh0OdLmM6o3MzS7zx/76nX7A5SOa48gelb7suOb/zvM/7XZtJ9EDrikscGHhXFTj+DiDv/mwNHH8YIp84HJFC5aDV2htJdAJ7/b5sEYW7RbJ77SL+inflWSIlGEu3MVfFoGDYAtKvKFIiMzM5OROvwaJ0iwbzERtfigdyBLxmaqYikUgHGBhUqYq3U4vmByut1uoXXzA56Hyk8JEhldWLmRXaabY+PUOEyKPu+voWz9r7kYr9rX6/P5gS2dlW7e+G2ZXkKyRMrmGz3ihhUyInT54881UXiQYvcBzHaziNjf/iFcKvVKRZ73Cg6+74VKW6mNhOPR7+zmrdhLGMKZLACB5N2Ddoi3sIe5Ej2SKddne9sGaRwQrKoZSIvzjYzlwdli2QyAown2AyImeIiGaLyejR6TgNkeF/hQb4shlAhFCtUvUNJLaD5mbkdg1fVPTai8iPVx/b/s4bUyrVNw+8TUy+69uULVKvra/3rllEoaUoUCzQSsnASipfc47A40xlVyknuFxOp05nseg4j+sXUeRPcQ3CBeIq69YDHR1VhHiFdSvPcw1dpYJNT8bGj5kCqg8e6Ol5443mgKpmW5YIb+Nttesz+rWePDPzVVeRxemsc5bn5VmAx5WMyBceF/AAX4X1IWf0wCegozWg2sp5BE6tVhchfkVFQkRZg5D0vLp9S5+qwpstQhaV2tdH5P6TM2cg4iyvq6srdxIsHk8yRyxOTzTqwVvnA1bVQ09R8s7vJ+Wt4KmQjgLD6C6r6qO+QxiBDeYvE9GPjPSvV0RGZ4gIPIgEAc9//gKPMWeKamuaYCxdrFjME8ENCG7F6NiqCtQkIHJvWkQ/ojeuT0SQ7JOjokg5NARqgmvTjY2VZ1j8NE1epnwxmJfBGW6r6fs05K5fKbJeEZmZGaUi5fAQNEAQLE5ylzPkrQWnK4KVIYmhXguRe9NVq2rdqtaZmZljEMmDCPUQVUSTOoJzbSIWwWZDa8zKiAhMIPIWIoJ1loxIAZvPMjcEiOw/diwlwmkyJsiUumj0wIFoNFqXHZN/iZUFzZeG5/VKIvI4RAAVMdlMGRG5UmrAwCOfuS6ky15orypSmlERPC5HB4g4okgfJA2g7QCN07MidU4LPggEQYNZjMfR6SYij2dERkwjKyMik1+XR6yyss1vTNtsTudIWgS3VZNB4F0YY0UiEQfWSWCiA9AQJT589kPKs3UCr4/A1iVgOiZ4HANURDS5763Xt5l4/j/OkeJGjADU3UNtu9MuK0XKIYKYpOBdLkckKQITBKUcEoBKpEQsroTEblc6BARE8EQH7C2y199/HOAsCiJiq9LzqzS/CulVarxMIf93D0p+b+UmtTxLRDWZLbI8JA5KlIjUQYVqLFPB+9FOt9tt74hadE7kFBFpeh87cPilIiN60yoReel5M7Mc/eDOnZvGtcxySlgAjwyyxsqdhWmRQADN71iDDrdcx5GQXClCLODhrANwSstQwQOddrt9oCMKJSyeDgy4W5qeoJvu92HlcRtv2rLl2mMt5Sam+qUCJoN2507ikDu+IiZt1aB2Zeg0aZGKwI8QUetIEhQVpTSgRLKd4BIsOhELQBl9BoWKoWn7BKB1I4WOgQG7+2XsJgIcBexpwqpM6crRr1yazaBEKg0eUqdebvZVasSS5og0G59felXk6bGWdfLHs2azWm3GnwaYpEX4pAiHfwL4SIM5SAtJ4AQ5jrN4MNAELqMD0107u3fvW4QebMeNYZJ2zapV66dPjUMsfQ7XcJlp1ktMhqqhVQeNoA8XZyZAhJpwEAmFiUjE4QqGzWmq8WjvVwNzY2ORLuQTOIgAQeB5G3q/RKddwmLDp0nWJHuZlWyc2G2+1nxEXpMssEM+hikaDy/Lnt2Z7mJcupqIKhAIVJrVYkTQ9KTp97rUjT51q683Zg7G23tj3eHW1pg33tgdr437NFywUYjH/R5za6u5OxaMx9Vo4xIJpcTNGsg+8L592GuUXXx319nplSIKr9cbFnvFmJpJ4R08Ur0iLSpzU6WhwlV79gARUQPUK0KWiJHrjat94ZC/IdjdjmlnazUO6vlj3tri3laXS+3jfEGvQ9cbCwcb28PBWqUSFm5DS339PqAg7Dsfv/jumytFtKFQqL9t0Iw2tDK7EcplVsC2yZOSjczqImVlZYNYKIVGiqRIsb893h3yd4eK1eZYt/+l1t7ixuJWX3Vt3NsYdrl0Pk/c542EfLFQvDoYb7TYRYukh/izuHg+1pclUmLA1Se7+daa7kG8vBbe1hJCVeVqAxbaagX2DxaBBrDMBB0cqr7OaREzuxjJrdZZ8KvDsITMHMnc0eHwRJ3nY2iF3e4WaADqkmQxfP7vC9kiSjmTK2OScGFmFXybKNc4CMmmx1okIkVcUgQXq6PtEZpZmsQ6MjehBHVJ6GSY4omK5JVHO+wtokdLixvQF+D77xcX/84SUaB0I4DIX+CgBV0IkkRNPVIiFIigp8QbGYuUCOkrD6RAl57Ebe9EG2x3Uw5euPDbhfWZj1ScPn36oIVWLWKQlxRxUiP6jpOU0jVOAGJXGUmQZbsUdrfoQccrHfCgnDq1tPT5+ogE9pedPqgjFUs0EK/e4rRw4hIJRKCZ7eGBB1l8xEJqB1ZS7QQiQoEORDrdyWr2PfhtnUTKIFKkBmmRBswsoi6eoOGIUzr/aZYjFsAhPqEWoRoREUJS5JNOeFCWixgYeYG8wCBn5HKcLJUUyFn5ZqZKyjIFOHSK4nVAWy2INKQ8dGSix7sS2EQg6I0eOOFEB3LCSC89gUAk9DYe/T96cpcDJiQvUiIodzowtGcNoEX2G8jkCE6X8pJCPWPIL1QaZRJjYSGvwdlZJcMXkqMg1ycyOTpaUYmKlRQR9Ep6yEf2Ms43sE8/LRFhk9AXyiqbpkFtVnM8DI0JN+68OxUQpLoRy69Vj5E1bZb95ofq2KeZiBjYwlxWK92sxLNEppQZZFpGKzXIJaykimWvT2QUfNXVlUwRvgobHISmvdhxo4dk8JqcqMFfLexexZZPYZVpC91t1PBAL9Hm5stgIpKwlXZ1YcueHpF45A+wcJUckUpXLq9jJH+dIsdycg5jl4q2WUIVm68AuduaerB5iLEfHBQlAGebmiD3Ku70Y4VVIyZatxARnjcqceBGa0iadDpKx8Zg8uQLJtPIiO35msqhI+uT7Dlff3wzdqyoCOeQ5FONbU17e6hHE+IhiiBGe58hW+6FpD5jAdEGjEYs7j4iy1XkaluIBXA1jKVN+Ivn3tz1Xtl6nUW59Nns5Q0bd+3a9e3gccJPR6d+mp09vvv48RPTn01MHA0Fg6HQid/BxMTE4Yf6as4eGR8/e+j5Q89/dRYcGVr06IK7gwcpF8axcwQ2btgzPDx8+tz83Nye9RHZcOny7OU/cmYwCD50orZ2dnZ26kRte3//CcLU9JS3NoQvqoanP/uMaBxemN+TYRiP+T3f9bb3h0LBv08BqAQmJ3/8cXT0GFzmITI8DJFbmBsMRO4cnrv8xyXi8fPvR48iGD9NHZ0CKE/T1/g5+ju1WJhbOLychYXDE99MT+M/4NMHRU6dTJtsJCJ77r7pptsUzI2l5Pab7sjZcOnSxgDYMze8QJgbnpsjD9xLVA8RFMH8xqx4zBM2EDbmkL/zNUmTpcmUyZunz8HmDvJd3RLmRqK4hXzr+J4NZX99S9jx7Y4d3+JBilcQIOP9K98WP1mxY8f+wNLSKbC0tHRukqpMTj537tz9d8EDJrdLbiC33HbrTf/zTztn05q4Ewbw7/CYSSYoAVtTI8E0DZjT6qGXRRERAjkU9CoRLIJCTl5iEXuXqiAeFikeCgu97aW99XP9JxN1zeo22epm/S/7a5uXdtLpj8zL4xOnp0cEa+YjqCSKNfPRVBLJmvkoKolkzXwUlUQSPURRyd8rgpnMHnRYodNTziua2Fv0RESyyfgusx6sGNHzO+/ifHwP2omIXOVje0jDCo2e3oNLIr6vaOWERBp+NiJIELo+kZ2i8ZMSyZ8xPsprkVEy2fCLJNuMj2WwCE7JsoxhB1E8ukhSBR83a5EecfCLfGbARz1YhHutvjyz4KfUgYH5W0R0wYPHnghtV73ZjLSd+Gx2tyXiKxpGZKwDCG3ANh62tSvAj1qW7b8xjArM1NLhsa1dHE+Ef0163NprkX4y+cIw6VhsmWDULRF+si76GE6knpMSmRZWnpRxpXnDO0WjftWryo7FjueVr2jcNV7R0USELzGPeG0tUozFqnTUqmOALRHh07qoEbJpFZ9tT6SlwlKtcsCgQQ8ca/4MQivRkuFG/s0ib6JYicVueJGAPipCm1aCinxzRd444D0RKw06Famzv1nk02TymYy4E5dvyiEiQn04XYlo/bM3brgQHIupd6ZVFIGIj7uPivAWItvL/nyKDR2aOqr1zwClL6QrSKQ1EQwRauL/QSQU/0T+XpFCzGNmBYp8PmURbDQ9atyWSLnZ/BaLTabN9JYImm+KhhPBx/j4cKzlnxDPqciHYi2M0THA+A+LEA1R5w9FFxWEg0XiraqP25+LzMY7RQNERAzHAIsoWGSH/SIfeYWIEX8kER7hPyqipBDZif63NOkcvIcCgFhwkckhICCglBIgIvfSe8hRkfuXl6/p9HPxZUxFhL1Fz98VoSt14HE6ryHYwJnwc/RaBZSRYQCAZVdEZjSgIrK4IxISbx5pAdTW80hI/CKsKzC4gFGqPbXBbI6QOprauDa65i3HsEY78SLSNbjogIUASdDJKIwnwp6EiFkbOKjNjlKOaEqOmjBzl4rGGVBJZU3wkUiAqAHnMA86APAaBi6kiOBoDpG3NB8SgNVqTajI5bfWkoroI80HF/6OdNpDGLU12UKd3EjhzIEKTskEDTE2+MiUiAjZDacYQLQE8IuEj7UoW8mHFhD2d/a48QtNC0/VXrunEpHLoW3YjNNx2B0RChVh5znIogcjx5yMCO3sZKMiucQhAXhRyTI6cFmEdVBB5+FHMEtEVAAVl0olHpAerrNz1dZbV3t4a41nJLhqrXjWXNKt1ldN61CR24rWj/ttq7RQ9v3hl4ocTrAIIJRJxvPnSL6NxQfIQ1zEXRYIPcTjYzqPxMh57AdoIS1wHjmc4HmEwuVjjWvgiUhnXf9LjOCbECkfmBAFQZDZQ5HJbwkhItefyo5pJdcil6ZdJ23p6alomgvSO2zTeXK5I/fglR5t7k3h6Wn+vggvM5lS9kAyDMuHEAGM5UKMsBIhFoQ+xt9TptilRKLfBCbw97EVDsYQcEcwHAMUfEcowpctkRtv4Pp57lfciDQBgkRQRCIoKxHarwXC/VxyGRcKJJJcSJLXU2ZfCnXv4kmhrAJkpbY3WCcLhQdJSoUZtXiZfEF4ELvag8CyQqjOnv08IyRzOkEuz1yaulCluWuvK9wIugIUUgQD/2U2W+UpdH08m41CzCOXRsdCFxAa3ulYbqxZyeKrnGl5w2+oML5xTf1fYy4WHbU21DFss5tFCZ7Z59dwIdpqc9SGUHhBIyhn7p81SISd2Qkz0qgwoFbBRZMui/QgTwwLhdallPlB5JY2RNKuvhUKzRAivKWV0EOiBhaCYNRrRkvQ/3jsivAmDi1CIK0rs370MZ41+vSAJE/GguDkZ3d+kfijQJBvZrOpwKMQIgkkdlMPCQMsBYLhrkQaNHoi7QsIL0JVSrtPrNZh/P0PImdAQEtv1Aoh0jFzFTG0CKyCRkxFHDGcSLaQ97jdtJ9W/lMFQGTZdD6/oCIF9jtXRMRkWQRonM+PWFYM0dlBUDHoIAKGcIg6APlEmO5CdXbErUGwIsVxPH1iVeQ4gYrEk99x+3o+mSwBCBzXSiadUwka97NOPgAV2YO0yWudSNCIU/tAAP1GowguRr4xIzel4W5jjRX5LACfSo0beS1gZk+x3OGwwUFj5q6wy/0IgJUkFVz0K8nt72eSGY8lB9IKBPgtcGanIiqTOBRGlXnlg3ktHzsp061R61SCRlck7scTQaILAsCiOCQ5eVmUNiKKSEjdxOPW6QSNRCRv5ny8UpHuxKUPYE4mBSJ7T7YrEWU8IdyNcjnaroJHLZyb5zCEhGkD5GqG6F43B8EeiAGdPSgb789rUTZh/C8mH3ImzCVgGAA1AQGwZgXA0cFl8AwD4fwxYPiNRoTO7E0GkDIwjbbk2DkIQNQAHqwOBmDsHtnMr34hHaS/lT2Wwy2RpW2ny+WFYXfJ5G7ZhOkNLeTYLsSf/NgMI8IBxj0QHXX0KIYREbDBgTJl0iJw1nmASMDD0N1sPCGRpIXOgBI6jG93kJnVZHWeyUi1ECLKQDRUJA7bz8xQuLZPR0TJDTrADoyU2hkwEAB+BDjrtOEaAZjADUz+YJHkcnn/M5Hq8oZsz+ZG4n2RCGKtYBGP/SKFTWc/kaDxcJETCRrfE7nrufQXiyLd9hHoGv3Wgpz0upkQIog/TqaR59FhnR3WrxCHJD7RgULnESnUgx6aamRV7jBUNkVjxuhFjv+cHaFwIihd9Ph6vhaxi8WXft8mJYrFLgD3tZhGkOr2eyIVqfbTiSARCsYYHQzG+MC3y7bAB+nsDS58iELBcBT+vAglEpFGv+vjnoq0u11iNKHfaQJwle4IgfBcLepUpF5dkMtKJyWyi//9Wvs7u/cK8Z/I0UWYenmXZQ1gtHVeBcjelFsKeKBFeckA7pfLndMR2eZ/vTSJ8E/kT4pEsmY+ikoiWTMfRSWRrJmPopJI1sxHUcl/Q8/cLJgt7e8AAAAASUVORK5CYII=", + "image": "tb-image:Y2FyZHNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IkNhcmRzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC/VBMVEUAAAAuTILg4ODf398dMkk0SWLg4ODg4OAyP1UrQF8sRH2/v8fDx8dFbHsDCBhKWGvDxsYaNET///8AAADg4OD4+Pjn5+fj4+P6+vrv7+/q8voKCgre3d3R2eft7e3TzMjP1+UdHR3My8vQzs7r6+vz8/PQ0NCKiorS0tPb29zm8Pnp6enQzMrX19j19fXNzc3m5eXW1dacnJzT1NnT2+nJycrx8fHf6vTMztHh7PXP1ePTzswbGxwWFhYsLC3Y4u7Wzcjj7ffT1NTS2OHl4NvZ1tXPz9QhISHVzst3d3jZ2drX1NNKSkrP1N6Xl5fn495tbW3d3uHW0tGysrLs7/HY3OG4ubnl7vbi6fLL0NhSUlLd5/PS0NCqqalXVlbp7e/Zz8rs6eXg4+XLzM0yUI3e4ubf0coSEhLc5vGsrKynp6bl6OvV2eBjY2MkJCbO0tvHxsbT1t3Q0dfBwMCSkpLj5efq5eG1tbU2MihVav/d3+Ta2Nfa09COjo7W3+vMztXl6u7Y3uakpKSEhIQ7Ozxynsy7u7ve2dbDw8QxMTLu8fPg3Nng5+7IzNJLWnHi5ekgN2rw8/ba5PDU2uSfn59AYZqamZlcXF03NzejoqHn6/Ld4+oODg7d2dqvr6/q8fbu7Oo7W5MpKSpsmMdxcXLe1dG9vb2Sf25OTk/j3Nbl1cpMcKZFRUZkjsA2VJJ8fHwiPXJGaaB/f4Dd1dQqRXy3zuWZqsmOiodRYnllZmd1hqJpeZPW2dlchblzdXZBQUEPIDjFyM9WfbVghrBRdq6Xn6tpaWkSI0Oqu9qyxN+4vMJ0gZlteIgcNGANFyagprCgs9GAk7mSmKMeKjiwt75YfalXZoBBTmWssLaGipFBOS4XJCVfcIqBhIl9bFvGvrXAtKCOk5gKGS46JBilq7RVWFwjMyuVoOllb3lsXk8lFg5kdvp3hfK1r6qyo5Gax6iFkKViiJufk4alquKTjIs6cIRXNyiv1LqKpqjE38t8jLCzq6GGvZdrr35uroH7pmE0AAAAEnRSTlMA/u8g/v6/r/4gtyC/v7+wr69GtHW3AAAd4UlEQVR42sTYb0wTZxzAceaSLfvzYrlcr60pjacFet4oLRVE0ris3SQW2u2UIuCk8bBGaRFaqIaillqnlT+rUqUoAXVDQyIbDNwSFifBOaMuJmrYoi/IQjLiC7fwwhfGZVv2e55C/+yYDBj4FcqhJN7H3z3PXU1Keu31ly6/FNeOqc5HOxXpS9ylSD9G+07Qj9ClS1+eeumTQCDw1htJ0MuvLCMXsVdfBgc69S9wU6eNTxwXd+bXcddQ30b6HPrs88+m+3gq/A380bfXLp3/ieM4feANcCyTEotZ9rKXk17HjggEU6KKOMf1eMe1aQaWJChiGJBcBwjJ8/o3k5JezSYWN+krSZd3YIhwItiROBEMAUdMMjUNYSC5duonjud5LpCURBKL3TIMOX/v65m6+/Xd+G7GdyvWVzN269bNu6M8qnopICSCfPHdiriSI61ZAx/THYz2TrTW1tZd0NZIa6dat3a6ra0HO6xLC9lxT+jAlsQijoNCSMwBkGhrd3U0F/Jutztr6SZyL3cGSdxEkhEDIFBsIPEQgeJd+Nza2lwYhOIhu8X4b20LwGsFIdpNRMpm/yeI0IGbYiSvCPlwiRBwQALHu9OtbS5uRJCRGGRb9UkKvvSpurJOp/trKtKIrmrjvt27uw4UHTL6KVXRESYzq8tMmWuNJ2vmBcn914mAIzfX4bF5OI/NJpxIDJKoWL363XXNZxrtUHkUIjITqpMIQqzKJPz+AVEasc3ffcQ8FNj8oXlzLSGvbcuorTFfPX3EX2vOXNCllYwhiQNZkWuw8Dxj5NwdvpkhiQ5QoLasKxZAuovkZvhac2Bb0YHU0xfNFYS6oi0lbYBp22zO2EyI0vpWthUd8Ov9YSbt4nwWey4UHYlwIiYLz13RMnzQFz8SISTKwE1DWjAEV51Wm03MnqjtELzOExIbiWAiGKJVGnmbb03cNiyAJDhgJAjSYG/AkEVNCBFOBIpA0vUMQEKhkC8SWFqFA4k5lmNIA5SzlBBoSgGOaHGXFs8wHO9xOC0eWySPLxGS6Fi+/HkQkhFJRKoUkUilEhOS7BQim0gRkbIUgkghRATJwrfzhkAz7r+xXQsEFqfTY2dZBSpoO4ivrRgk3vFciIQkSLVRQa7UGI0MQZIMo2aMpDEdjtFvyFQaGTN/CPRPiOk+ZFqTHDI4LD6Lz2dxVjndVqlUWihtlNptsWvrORCr1SqciIykjGqVlFRQarGEkrIKmVpKkWqKgBcZQakYsXwBEKHENB6euG8ImXy+kNPncPgsJSUON6WQS6WKbMpuC9+OjOSdXbveAcmMECu0xGsElwhZkTvWZ9s3XHpooKCndKLA2Rtur9ndU7Cvb7h2qDL8oL3y+6sHh1vae0dKS+8nQJa/CEg/vLsFSEyCizhMYwPusH8g7DenFrA9qT2OAX9PWt/QxpPqLnO7d+I2hgxvfdg93otG8p8hKvgFqxqtbfgKC1wk+X8gvwMhnmIwIAuGUGW1RVn+NmWBtVTZYxkYK8jcfGRjZn17dS+GrBlu6d01NjY6ujYi2bJl9VlwxCCF1sINAkhWdhbLsmIxwZByWBxyjZols0npAiGXL1/+3RQLLAan0wA5nVXjB6jysvBJc4V+qOGGviBcGrjYE65IO91UWdkz8jA80hMua6nsKJmYGG9ubi4uLl63ZcuZM2fPRiEnrNbCGSCaDFKsJ0kZQBjSKBOTJMkyYvXCIMcRpKpK63JV4cDidGpR6fDJUQqWoliFtQH2n1JLlcOt2PvBe++RanfQ4wv5LBzalW0dvg6oGUI/VhgHKYSUAohIJVJJRSK4wuQSuUgigWOJSq9YIKS/v/+bKpfLtQHldQEFHCiNRo8hajWlUFihLosWIOe2792rZhV2uMs7mCYZUCB8n4GCdlYxI2T2JAtcI3f6jx//pgQ5cnJAguaCJ+Jy6fMBYldQYlLMIojcbtE63Qr5OQUL39s9cHsxHu2Uybjp4AoRq7efAwhOAFnM8EQAgh05q1aBxOXSolzKTflKDCFlIJFDAME3RDnc2OV2D6yi+qOdnQyDLCRKLBZCGhuXDtIfg6RugJR6SPnRpk0Iwqpl9VdISgGQoKUKIPBvHFEBJL3eCA8VDIcvLj4Ib9BhPcVDwNGoX3II5EUjwW2IQChW3PS+xohGonADhLcrcCxvMZicmveNxvpOBJle7cFESOOSQ0Di9ZbgqnCuDUoEcVNkvV5/hXPb7SxAtBYPj0IPkCYT7Afp7w8ODnZyNlBs3Vpc3Gx/8RDswAYTqsqFIBY3z1zR6+H8bbzbbYGt2eGw4BwIos/XDF64cGGwkww2FEMvEnJnerF7ESMykTgI73HAynca4Pxt6GoyoSdh2Gl9ITiGjS3/ws6dO/cflfENoICEkBNLBTmOtt8NUAkASrw5OS6XExzTE7HB2RtQEUiuwwc3C6u1oSOEIJv2YwgaSXMQgsUuj4egtHOBpIhmuduoVLNBvC4TQFrKc1KVWm0MEvSFsMMZKKrRwk84PEHWrQyMWGAk2vz9b//284X9g4NHGY7LKjqiVMP2uwBIoPTYsUoSeYqGqomZUj2TPwfyQ4kXdi0vLI+W8vKM1FTYuUpu3kIQxm0P2izQ2GGapj/9cAQg7gPDeXn0sRsjBq3m0Hpd3tBHRribpB3Oo+m84bYoZPXcIYE9vRW1ew6LxJXteXlFxExd+fPpv0PuIAjefHPKAZKRsWqV93Fd3ZNNH+kZimXdPEl20/T6snZaVwCQqzTdHgZXaZVTf6zg9sO8NoAOIQWy1CaukRMn5rDYh/I0BHEjL6u6oLYbIDM22UhEgmuwUHhpASQjAyjTkCd1T359jCBuiqLc6mqaLhOLmTKd7hfDbZoOe4KOME3/4gzQ46Gq9RtDoXFwtdg6Wnrp9cp4SLDhhD3nP0OKuiQEsTEvFQ7TAPLc3H8SfzyTPrUmPmvhSysaSB7V3XpcB5eWAz0K8ryfpgM8yT3Q6R4a+nR0Dfx3EDp2rmrvGR3T3Q8Z2uljLo7jO0ZoOu2fkPI57VqKY8PE7BDRU8mk6q8/rE+fSRIngiH4TuJtgfWe86jO+6guH/ZdB4ozD3UbORlzG05+xXjlkCMUWoOOnemZn+p0E6HQqE531cHISI9tmC6LXyNB+wlr1pwgN/bUzArBTUomJwlyUiWAYAeWwGMwnogG7h8oh4UjSU4mY9J0utHk5JADQR7CsVZTX5P5AG4s47QuM93YBA/17sT7CHuO2j4XCAAyCQFkloQQTMEvj+rgQ6ONOIyMDLdyPV0aSoa3ISBpWa8rNWjzBzVajZHjBnS63en1R5uamsTxkIbGc9s/YFfOAdKW10fMF3IHdi0vLPbI2xEXQFwuuK5+fVz3N/tmF9NWGcZxP5jOCzWetD1t0kDaA93BtqOlKcG0IqntKFQptoq6nDqQqCnMWqzSabuosMAMsiVKqpWULAtjYkwweCFzZMLFmA6FBAhMh0OdLmM6o3MzS7zx/76nX7A5SOa48gelb7suOb/zvM/7XZtJ9EDrikscGHhXFTj+DiDv/mwNHH8YIp84HJFC5aDV2htJdAJ7/b5sEYW7RbJ77SL+inflWSIlGEu3MVfFoGDYAtKvKFIiMzM5OROvwaJ0iwbzERtfigdyBLxmaqYikUgHGBhUqYq3U4vmByut1uoXXzA56Hyk8JEhldWLmRXaabY+PUOEyKPu+voWz9r7kYr9rX6/P5gS2dlW7e+G2ZXkKyRMrmGz3ihhUyInT54881UXiQYvcBzHaziNjf/iFcKvVKRZ73Cg6+74VKW6mNhOPR7+zmrdhLGMKZLACB5N2Ddoi3sIe5Ej2SKddne9sGaRwQrKoZSIvzjYzlwdli2QyAown2AyImeIiGaLyejR6TgNkeF/hQb4shlAhFCtUvUNJLaD5mbkdg1fVPTai8iPVx/b/s4bUyrVNw+8TUy+69uULVKvra/3rllEoaUoUCzQSsnASipfc47A40xlVyknuFxOp05nseg4j+sXUeRPcQ3CBeIq69YDHR1VhHiFdSvPcw1dpYJNT8bGj5kCqg8e6Ol5443mgKpmW5YIb+Nttesz+rWePDPzVVeRxemsc5bn5VmAx5WMyBceF/AAX4X1IWf0wCegozWg2sp5BE6tVhchfkVFQkRZg5D0vLp9S5+qwpstQhaV2tdH5P6TM2cg4iyvq6srdxIsHk8yRyxOTzTqwVvnA1bVQ09R8s7vJ+Wt4KmQjgLD6C6r6qO+QxiBDeYvE9GPjPSvV0RGZ4gIPIgEAc9//gKPMWeKamuaYCxdrFjME8ENCG7F6NiqCtQkIHJvWkQ/ojeuT0SQ7JOjokg5NARqgmvTjY2VZ1j8NE1epnwxmJfBGW6r6fs05K5fKbJeEZmZGaUi5fAQNEAQLE5ylzPkrQWnK4KVIYmhXguRe9NVq2rdqtaZmZljEMmDCPUQVUSTOoJzbSIWwWZDa8zKiAhMIPIWIoJ1loxIAZvPMjcEiOw/diwlwmkyJsiUumj0wIFoNFqXHZN/iZUFzZeG5/VKIvI4RAAVMdlMGRG5UmrAwCOfuS6ky15orypSmlERPC5HB4g4okgfJA2g7QCN07MidU4LPggEQYNZjMfR6SYij2dERkwjKyMik1+XR6yyss1vTNtsTudIWgS3VZNB4F0YY0UiEQfWSWCiA9AQJT589kPKs3UCr4/A1iVgOiZ4HANURDS5763Xt5l4/j/OkeJGjADU3UNtu9MuK0XKIYKYpOBdLkckKQITBKUcEoBKpEQsroTEblc6BARE8EQH7C2y199/HOAsCiJiq9LzqzS/CulVarxMIf93D0p+b+UmtTxLRDWZLbI8JA5KlIjUQYVqLFPB+9FOt9tt74hadE7kFBFpeh87cPilIiN60yoReel5M7Mc/eDOnZvGtcxySlgAjwyyxsqdhWmRQADN71iDDrdcx5GQXClCLODhrANwSstQwQOddrt9oCMKJSyeDgy4W5qeoJvu92HlcRtv2rLl2mMt5Sam+qUCJoN2507ikDu+IiZt1aB2Zeg0aZGKwI8QUetIEhQVpTSgRLKd4BIsOhELQBl9BoWKoWn7BKB1I4WOgQG7+2XsJgIcBexpwqpM6crRr1yazaBEKg0eUqdebvZVasSS5og0G59felXk6bGWdfLHs2azWm3GnwaYpEX4pAiHfwL4SIM5SAtJ4AQ5jrN4MNAELqMD0107u3fvW4QebMeNYZJ2zapV66dPjUMsfQ7XcJlp1ktMhqqhVQeNoA8XZyZAhJpwEAmFiUjE4QqGzWmq8WjvVwNzY2ORLuQTOIgAQeB5G3q/RKddwmLDp0nWJHuZlWyc2G2+1nxEXpMssEM+hikaDy/Lnt2Z7mJcupqIKhAIVJrVYkTQ9KTp97rUjT51q683Zg7G23tj3eHW1pg33tgdr437NFywUYjH/R5za6u5OxaMx9Vo4xIJpcTNGsg+8L592GuUXXx319nplSIKr9cbFnvFmJpJ4R08Ur0iLSpzU6WhwlV79gARUQPUK0KWiJHrjat94ZC/IdjdjmlnazUO6vlj3tri3laXS+3jfEGvQ9cbCwcb28PBWqUSFm5DS339PqAg7Dsfv/jumytFtKFQqL9t0Iw2tDK7EcplVsC2yZOSjczqImVlZYNYKIVGiqRIsb893h3yd4eK1eZYt/+l1t7ixuJWX3Vt3NsYdrl0Pk/c542EfLFQvDoYb7TYRYukh/izuHg+1pclUmLA1Se7+daa7kG8vBbe1hJCVeVqAxbaagX2DxaBBrDMBB0cqr7OaREzuxjJrdZZ8KvDsITMHMnc0eHwRJ3nY2iF3e4WaADqkmQxfP7vC9kiSjmTK2OScGFmFXybKNc4CMmmx1okIkVcUgQXq6PtEZpZmsQ6MjehBHVJ6GSY4omK5JVHO+wtokdLixvQF+D77xcX/84SUaB0I4DIX+CgBV0IkkRNPVIiFIigp8QbGYuUCOkrD6RAl57Ebe9EG2x3Uw5euPDbhfWZj1ScPn36oIVWLWKQlxRxUiP6jpOU0jVOAGJXGUmQZbsUdrfoQccrHfCgnDq1tPT5+ogE9pedPqgjFUs0EK/e4rRw4hIJRKCZ7eGBB1l8xEJqB1ZS7QQiQoEORDrdyWr2PfhtnUTKIFKkBmmRBswsoi6eoOGIUzr/aZYjFsAhPqEWoRoREUJS5JNOeFCWixgYeYG8wCBn5HKcLJUUyFn5ZqZKyjIFOHSK4nVAWy2INKQ8dGSix7sS2EQg6I0eOOFEB3LCSC89gUAk9DYe/T96cpcDJiQvUiIodzowtGcNoEX2G8jkCE6X8pJCPWPIL1QaZRJjYSGvwdlZJcMXkqMg1ycyOTpaUYmKlRQR9Ep6yEf2Ms43sE8/LRFhk9AXyiqbpkFtVnM8DI0JN+68OxUQpLoRy69Vj5E1bZb95ofq2KeZiBjYwlxWK92sxLNEppQZZFpGKzXIJaykimWvT2QUfNXVlUwRvgobHISmvdhxo4dk8JqcqMFfLexexZZPYZVpC91t1PBAL9Hm5stgIpKwlXZ1YcueHpF45A+wcJUckUpXLq9jJH+dIsdycg5jl4q2WUIVm68AuduaerB5iLEfHBQlAGebmiD3Ku70Y4VVIyZatxARnjcqceBGa0iadDpKx8Zg8uQLJtPIiO35msqhI+uT7Dlff3wzdqyoCOeQ5FONbU17e6hHE+IhiiBGe58hW+6FpD5jAdEGjEYs7j4iy1XkaluIBXA1jKVN+Ivn3tz1Xtl6nUW59Nns5Q0bd+3a9e3gccJPR6d+mp09vvv48RPTn01MHA0Fg6HQid/BxMTE4Yf6as4eGR8/e+j5Q89/dRYcGVr06IK7gwcpF8axcwQ2btgzPDx8+tz83Nye9RHZcOny7OU/cmYwCD50orZ2dnZ26kRte3//CcLU9JS3NoQvqoanP/uMaBxemN+TYRiP+T3f9bb3h0LBv08BqAQmJ3/8cXT0GFzmITI8DJFbmBsMRO4cnrv8xyXi8fPvR48iGD9NHZ0CKE/T1/g5+ju1WJhbOLychYXDE99MT+M/4NMHRU6dTJtsJCJ77r7pptsUzI2l5Pab7sjZcOnSxgDYMze8QJgbnpsjD9xLVA8RFMH8xqx4zBM2EDbmkL/zNUmTpcmUyZunz8HmDvJd3RLmRqK4hXzr+J4NZX99S9jx7Y4d3+JBilcQIOP9K98WP1mxY8f+wNLSKbC0tHRukqpMTj537tz9d8EDJrdLbiC33HbrTf/zTztn05q4Ewbw7/CYSSYoAVtTI8E0DZjT6qGXRRERAjkU9CoRLIJCTl5iEXuXqiAeFikeCgu97aW99XP9JxN1zeo22epm/S/7a5uXdtLpj8zL4xOnp0cEa+YjqCSKNfPRVBLJmvkoKolkzXwUlUQSPURRyd8rgpnMHnRYodNTziua2Fv0RESyyfgusx6sGNHzO+/ifHwP2omIXOVje0jDCo2e3oNLIr6vaOWERBp+NiJIELo+kZ2i8ZMSyZ8xPsprkVEy2fCLJNuMj2WwCE7JsoxhB1E8ukhSBR83a5EecfCLfGbARz1YhHutvjyz4KfUgYH5W0R0wYPHnghtV73ZjLSd+Gx2tyXiKxpGZKwDCG3ANh62tSvAj1qW7b8xjArM1NLhsa1dHE+Ef0163NprkX4y+cIw6VhsmWDULRF+si76GE6knpMSmRZWnpRxpXnDO0WjftWryo7FjueVr2jcNV7R0USELzGPeG0tUozFqnTUqmOALRHh07qoEbJpFZ9tT6SlwlKtcsCgQQ8ca/4MQivRkuFG/s0ib6JYicVueJGAPipCm1aCinxzRd444D0RKw06Famzv1nk02TymYy4E5dvyiEiQn04XYlo/bM3brgQHIupd6ZVFIGIj7uPivAWItvL/nyKDR2aOqr1zwClL6QrSKQ1EQwRauL/QSQU/0T+XpFCzGNmBYp8PmURbDQ9atyWSLnZ/BaLTabN9JYImm+KhhPBx/j4cKzlnxDPqciHYi2M0THA+A+LEA1R5w9FFxWEg0XiraqP25+LzMY7RQNERAzHAIsoWGSH/SIfeYWIEX8kER7hPyqipBDZif63NOkcvIcCgFhwkckhICCglBIgIvfSe8hRkfuXl6/p9HPxZUxFhL1Fz98VoSt14HE6ryHYwJnwc/RaBZSRYQCAZVdEZjSgIrK4IxISbx5pAdTW80hI/CKsKzC4gFGqPbXBbI6QOprauDa65i3HsEY78SLSNbjogIUASdDJKIwnwp6EiFkbOKjNjlKOaEqOmjBzl4rGGVBJZU3wkUiAqAHnMA86APAaBi6kiOBoDpG3NB8SgNVqTajI5bfWkoroI80HF/6OdNpDGLU12UKd3EjhzIEKTskEDTE2+MiUiAjZDacYQLQE8IuEj7UoW8mHFhD2d/a48QtNC0/VXrunEpHLoW3YjNNx2B0RChVh5znIogcjx5yMCO3sZKMiucQhAXhRyTI6cFmEdVBB5+FHMEtEVAAVl0olHpAerrNz1dZbV3t4a41nJLhqrXjWXNKt1ldN61CR24rWj/ttq7RQ9v3hl4ocTrAIIJRJxvPnSL6NxQfIQ1zEXRYIPcTjYzqPxMh57AdoIS1wHjmc4HmEwuVjjWvgiUhnXf9LjOCbECkfmBAFQZDZQ5HJbwkhItefyo5pJdcil6ZdJ23p6alomgvSO2zTeXK5I/fglR5t7k3h6Wn+vggvM5lS9kAyDMuHEAGM5UKMsBIhFoQ+xt9TptilRKLfBCbw97EVDsYQcEcwHAMUfEcowpctkRtv4Pp57lfciDQBgkRQRCIoKxHarwXC/VxyGRcKJJJcSJLXU2ZfCnXv4kmhrAJkpbY3WCcLhQdJSoUZtXiZfEF4ELvag8CyQqjOnv08IyRzOkEuz1yaulCluWuvK9wIugIUUgQD/2U2W+UpdH08m41CzCOXRsdCFxAa3ulYbqxZyeKrnGl5w2+oML5xTf1fYy4WHbU21DFss5tFCZ7Z59dwIdpqc9SGUHhBIyhn7p81SISd2Qkz0qgwoFbBRZMui/QgTwwLhdallPlB5JY2RNKuvhUKzRAivKWV0EOiBhaCYNRrRkvQ/3jsivAmDi1CIK0rs370MZ41+vSAJE/GguDkZ3d+kfijQJBvZrOpwKMQIgkkdlMPCQMsBYLhrkQaNHoi7QsIL0JVSrtPrNZh/P0PImdAQEtv1Aoh0jFzFTG0CKyCRkxFHDGcSLaQ97jdtJ9W/lMFQGTZdD6/oCIF9jtXRMRkWQRonM+PWFYM0dlBUDHoIAKGcIg6APlEmO5CdXbErUGwIsVxPH1iVeQ4gYrEk99x+3o+mSwBCBzXSiadUwka97NOPgAV2YO0yWudSNCIU/tAAP1GowguRr4xIzel4W5jjRX5LACfSo0beS1gZk+x3OGwwUFj5q6wy/0IgJUkFVz0K8nt72eSGY8lB9IKBPgtcGanIiqTOBRGlXnlg3ktHzsp061R61SCRlck7scTQaILAsCiOCQ5eVmUNiKKSEjdxOPW6QSNRCRv5ny8UpHuxKUPYE4mBSJ7T7YrEWU8IdyNcjnaroJHLZyb5zCEhGkD5GqG6F43B8EeiAGdPSgb789rUTZh/C8mH3ImzCVgGAA1AQGwZgXA0cFl8AwD4fwxYPiNRoTO7E0GkDIwjbbk2DkIQNQAHqwOBmDsHtnMr34hHaS/lT2Wwy2RpW2ny+WFYXfJ5G7ZhOkNLeTYLsSf/NgMI8IBxj0QHXX0KIYREbDBgTJl0iJw1nmASMDD0N1sPCGRpIXOgBI6jG93kJnVZHWeyUi1ECLKQDRUJA7bz8xQuLZPR0TJDTrADoyU2hkwEAB+BDjrtOEaAZjADUz+YJHkcnn/M5Hq8oZsz+ZG4n2RCGKtYBGP/SKFTWc/kaDxcJETCRrfE7nrufQXiyLd9hHoGv3Wgpz0upkQIog/TqaR59FhnR3WrxCHJD7RgULnESnUgx6aamRV7jBUNkVjxuhFjv+cHaFwIihd9Ph6vhaxi8WXft8mJYrFLgD3tZhGkOr2eyIVqfbTiSARCsYYHQzG+MC3y7bAB+nsDS58iELBcBT+vAglEpFGv+vjnoq0u11iNKHfaQJwle4IgfBcLepUpF5dkMtKJyWyi//9Wvs7u/cK8Z/I0UWYenmXZQ1gtHVeBcjelFsKeKBFeckA7pfLndMR2eZ/vTSJ8E/kT4pEsmY+ikoiWTMfRSWRrJmPopJI1sxHUcl/Q8/cLJgt7e8AAAAASUVORK5CYII=", "description": "Includes cards that display dynamic content based on data from one or more entities. It also includes static HTML cards.", "order": 2000, - "externalId": null, "name": "Cards" }, "widgetTypeFqns": [ @@ -14,6 +13,7 @@ "cards.aggregated_value_card", "simple_value_and_chart_card", "progress_bar", + "iot_svg", "cards.label_widget", "cards.dashboard_state_widget", "cards.qr_code", diff --git a/application/src/main/data/json/system/widget_types/iot_svg.json b/application/src/main/data/json/system/widget_types/iot_svg.json new file mode 100644 index 0000000000..fde568de95 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/iot_svg.json @@ -0,0 +1,26 @@ +{ + "fqn": "iot_svg", + "name": "IoT SVG", + "deprecated": false, + "image": null, + "description": "IoT SVG", + "descriptor": { + "type": "rpc", + "sizeX": 3.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-iot-svg-basic-config", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"IoT SVG\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":null}" + }, + "tags": [ + "svg", + "scada" + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html index 6940cffc1f..49a50afc30 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html @@ -29,7 +29,7 @@
widgets.action-button.on-click
@@ -38,9 +38,9 @@
widgets.button-state.activated-state
widgets.button-state.disabled-state
widgets.command-button.on-click
widgets.button-state.disabled-state
widgets.rpc-state.initial-state
widgets.power-button.power-on
widgets.power-button.power-off
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.toggle-button.check
widgets.toggle-button.uncheck
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.rpc-state.turn-on
widgets.rpc-state.turn-off
widgets.rpc-state.disabled-state
widgets.slider.initial-value
widgets.slider.on-value-change
widgets.rpc-state.disabled-state
widgets.button-state.activated-state
widgets.button-state.disabled-state
widgets.command-button.on-click
widgets.button-state.disabled-state
widgets.rpc-state.initial-state
widgets.power-button.power-on
widgets.power-button.power-off
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.toggle-button.check
widgets.toggle-button.uncheck
widgets.rpc-state.disabled-state
-
{{ panelTitle | translate }}
+
{{ panelTitle }}
{{ 'widgets.value-action.action' | translate }}
@@ -122,7 +122,7 @@ helpId="widget/lib/rpc/parse_value_fn">
-
{{ 'widgets.value-action.state-when-result-is' | translate:{state: (stateLabel | translate)} }}
+
{{ 'widgets.value-action.state-when-result-is' | translate:{state: stateLabel} }}
-
{{ panelTitle | translate }}
+
{{ panelTitle }}
{{ 'widgets.value-action.action' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html index c5b4b50272..3fffd3f424 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html @@ -16,7 +16,7 @@ -->
-
{{ panelTitle | translate }}
+
{{ panelTitle }}
- -
{{ metadata?.title }}
+ +
{{ metadata?.title | customTranslate }}
-
widgets.slider.behavior
+
iot-svg.behavior
-
{{ behaviour.name }}
+
{{ behaviour.name | customTranslate }}
@@ -51,15 +54,15 @@
- {{ propertyRow.label }} + {{ propertyRow.label | customTranslate }} -
{{ propertyRow.label }}
+
{{ propertyRow.label | customTranslate }}
-
{{ property.subLabel }}
+
{{ property.subLabel | customTranslate }}
-
{{ property.fieldSuffix }}
+
{{ property.fieldSuffix | customTranslate }}
+ settingsKey="{{ property.name | customTranslate }}"> -
{{ property.fieldSuffix }}
+
{{ property.fieldSuffix | customTranslate }}
, private fb: UntypedFormBuilder, @@ -105,8 +105,8 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control } ngOnInit(): void { - this.scadaObjectSettingsFormGroup = this.fb.group({}); - this.scadaObjectSettingsFormGroup.valueChanges.subscribe(() => { + this.iotSvgObjectSettingsFormGroup = this.fb.group({}); + this.iotSvgObjectSettingsFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); this.loadMetadata(); @@ -133,9 +133,9 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { - this.scadaObjectSettingsFormGroup.disable({emitEvent: false}); + this.iotSvgObjectSettingsFormGroup.disable({emitEvent: false}); } else { - this.scadaObjectSettingsFormGroup.enable({emitEvent: false}); + this.iotSvgObjectSettingsFormGroup.enable({emitEvent: false}); this.updateValidators(); } } @@ -146,9 +146,9 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control } validate(c: UntypedFormControl) { - const valid = this.scadaObjectSettingsFormGroup.valid; + const valid = this.iotSvgObjectSettingsFormGroup.valid; return valid ? null : { - scadaObject: { + iotSvgObject: { valid: false, }, }; @@ -162,13 +162,13 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control this.validatorTriggers = []; this.http.get(this.svgPath, {responseType: 'text'}).subscribe( (svgContent) => { - this.metadata = parseScadaObjectMetadataFromContent(svgContent); + this.metadata = parseIotSvgMetadataFromContent(svgContent); this.propertyRows = toPropertyRows(this.metadata.properties); - for (const control of Object.keys(this.scadaObjectSettingsFormGroup.controls)) { - this.scadaObjectSettingsFormGroup.removeControl(control, {emitEvent: false}); + for (const control of Object.keys(this.iotSvgObjectSettingsFormGroup.controls)) { + this.iotSvgObjectSettingsFormGroup.removeControl(control, {emitEvent: false}); } for (const behaviour of this.metadata.behavior) { - this.scadaObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false}); + this.iotSvgObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false}); } for (const property of this.metadata.properties) { if (property.disableOnProperty) { @@ -188,12 +188,12 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control validators.push(Validators.max(property.max)); } } - this.scadaObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); + this.iotSvgObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); } if (this.validatorTriggers.length) { const observables: Observable[] = []; for (const trigger of this.validatorTriggers) { - observables.push(this.scadaObjectSettingsFormGroup.get(trigger).valueChanges); + observables.push(this.iotSvgObjectSettingsFormGroup.get(trigger).valueChanges); } this.validatorSubscription = merge(...observables).subscribe(() => { this.updateValidators(); @@ -207,10 +207,10 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control private updateValidators() { for (const trigger of this.validatorTriggers) { - const value: boolean = this.scadaObjectSettingsFormGroup.get(trigger).value; + const value: boolean = this.iotSvgObjectSettingsFormGroup.get(trigger).value; this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach( (p) => { - const control = this.scadaObjectSettingsFormGroup.get(p.id); + const control = this.iotSvgObjectSettingsFormGroup.get(p.id); if (value) { control.enable({emitEvent: false}); } else { @@ -223,9 +223,9 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control private setupValue() { if (this.metadata) { - const defaults = defaultScadaObjectSettings(this.metadata); + const defaults = defaultIotSvgObjectSettings(this.metadata); this.modelValue = {...defaults, ...this.modelValue}; - this.scadaObjectSettingsFormGroup.patchValue( + this.iotSvgObjectSettingsFormGroup.patchValue( this.modelValue, {emitEvent: false} ); this.setDisabledState(this.disabled); @@ -233,7 +233,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control } private updateModel() { - this.modelValue = this.scadaObjectSettingsFormGroup.getRawValue(); + this.modelValue = this.iotSvgObjectSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts index 12231d2b1a..ebb8b908a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts @@ -16,15 +16,15 @@ import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models'; -export interface ScadaPropertyRow { +export interface IotSvgPropertyRow { label: string; properties: IotSvgProperty[]; switch?: IotSvgProperty; rowClass?: string; } -export const toPropertyRows = (properties: IotSvgProperty[]): ScadaPropertyRow[] => { - const result: ScadaPropertyRow[] = []; +export const toPropertyRows = (properties: IotSvgProperty[]): IotSvgPropertyRow[] => { + const result: IotSvgPropertyRow[] = []; for (const property of properties) { let propertyRow = result.find(r => r.label === property.name); if (!propertyRow) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index 9208efd68a..db6d0371f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -21,11 +21,11 @@
widgets.rpc-state.initial-state
widgets.rpc-state.turn-on
widgets.rpc-state.turn-off
widgets.rpc-state.disabled-state
widgets.slider.initial-value
widgets.slider.on-value-change
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.rpc-state.disabled-state
boolean; - actionType: ScadaObjectActionType; - updateValueId?: string; - updateValueType?: ScadaObjectActionUpdateValueType; - updateValueConstant?: any; - updateValueInc?: number; -} - export interface IotSvgMetadata { title: string; stateRenderFunction?: string; @@ -155,16 +149,16 @@ export const emptyMetadata: IotSvgMetadata = { }; -export const parseScadaObjectMetadataFromContent = (svgContent: string): IotSvgMetadata => { +export const parseIotSvgMetadataFromContent = (svgContent: string): IotSvgMetadata => { try { const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); - return parseScadaObjectMetadataFromDom(svgDoc); + return parseIotSvgMetadataFromDom(svgDoc); } catch (_e) { return emptyMetadata; } }; -const parseScadaObjectMetadataFromDom = (svgDoc: Document): IotSvgMetadata => { +const parseIotSvgMetadataFromDom = (svgDoc: Document): IotSvgMetadata => { try { const elements = svgDoc.getElementsByTagName('tb:metadata'); if (elements.length) { @@ -232,7 +226,7 @@ const defaultWidgetActionSettings = (widgetAction: IotSvgBehavior): WidgetAction stateEntityParamName: null }); -export const defaultScadaObjectSettings = (metadata: IotSvgMetadata): IotSvgObjectSettings => { +export const defaultIotSvgObjectSettings = (metadata: IotSvgMetadata): IotSvgObjectSettings => { const settings: IotSvgObjectSettings = {}; for (const behavior of metadata.behavior) { if (behavior.type === IotSvgBehaviorType.value) { @@ -285,8 +279,8 @@ export class IotSvgObject { return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( map((inputSvgContent) => { const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); - this.metadata = parseScadaObjectMetadataFromDom(doc); - const defaults = defaultScadaObjectSettings(this.metadata); + this.metadata = parseIotSvgMetadataFromDom(doc); + const defaults = defaultIotSvgObjectSettings(this.metadata); this.settings = mergeDeep({}, defaults, this.inputSettings || {}); this.prepareMetadata(); this.prepareSvgShape(doc); @@ -613,6 +607,10 @@ export class IotSvgObject { if (isDefinedAndNotNull(value)) { if (property.type === 'color-settings') { return ColorProcessor.fromSettings(value); + } else if (property.type === 'string') { + const result = this.ctx.utilsService.customTranslation(value, value); + const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); + return createLabelFromSubscriptionEntityInfo(entityInfo, result); } return value; } else { diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index 269ec33a5d..e3b3e12b83 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -70,8 +70,8 @@ - {{ trueLabel | translate }} - {{ falseLabel | translate }} + {{ trueLabel }} + {{ falseLabel }}
diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts index 0479b2f0df..83b6a4d432 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.ts +++ b/ui-ngx/src/app/shared/components/value-input.component.ts @@ -35,6 +35,7 @@ import { } from '@shared/components/dialog/json-object-edit-dialog.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { Subscription } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; type Layout = 'column' | 'row'; @@ -67,10 +68,10 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro valueType: ValueType; @Input() - trueLabel = 'value.true'; + trueLabel: string; @Input() - falseLabel = 'value.false'; + falseLabel: string; @Input() layout: ValueInputLayout | Layout = 'row'; @@ -96,12 +97,19 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro constructor( private breakpointObserver: BreakpointObserver, private cd: ChangeDetectorRef, + private translate: TranslateService, public dialog: MatDialog, ) { } ngOnInit(): void { + if (!this.trueLabel) { + this.trueLabel = this.translate.instant('value.true'); + } + if (!this.falseLabel) { + this.falseLabel = this.translate.instant('value.false'); + } this._subscription = new Subscription(); this.showValueType = !this.valueType; this.computedLayout = this._computeLayout(); diff --git a/ui-ngx/src/app/shared/pipe/custom-translate.pipe.ts b/ui-ngx/src/app/shared/pipe/custom-translate.pipe.ts new file mode 100644 index 0000000000..141d24015e --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/custom-translate.pipe.ts @@ -0,0 +1,33 @@ +/// +/// Copyright ยฉ 2016-2024 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 { Pipe, PipeTransform } from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; + +@Pipe({ + name: 'customTranslate' +}) +export class CustomTranslatePipe implements PipeTransform { + + constructor(private utils: UtilsService) { } + + transform(translationValue: string, defaultValue?: string): string { + if (!defaultValue) { + defaultValue = translationValue; + } + return this.utils.customTranslation(translationValue, defaultValue); + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 71f121ab17..45725f0331 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -218,6 +218,7 @@ import { EmbedImageDialogComponent } from '@shared/components/image/embed-image- import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component'; import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component'; import { WidgetButtonComponent } from '@shared/components/button/widget-button.component'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -236,6 +237,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) SafePipe, ShortNumberPipe, ImagePipe, + CustomTranslatePipe, { provide: FlowInjectionToken, useValue: Flow @@ -361,6 +363,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) FileSizePipe, DateAgoPipe, ImagePipe, + CustomTranslatePipe, SafePipe, ShortNumberPipe, SelectableColumnsPipe, @@ -614,6 +617,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) FileSizePipe, DateAgoPipe, ImagePipe, + CustomTranslatePipe, SafePipe, ShortNumberPipe, SelectableColumnsPipe, 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 b13d169cdc..2203545179 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3216,6 +3216,9 @@ "error-entities": "There was an error creating {{count}} entities." } }, + "iot-svg": { + "behavior": "Behavior" + }, "item": { "selected": "Selected" }, diff --git a/ui-ngx/src/assets/widget/svg/drawing.svg b/ui-ngx/src/assets/widget/svg/drawing.svg index 91bfb8b47d..55d60d04c1 100644 --- a/ui-ngx/src/assets/widget/svg/drawing.svg +++ b/ui-ngx/src/assets/widget/svg/drawing.svg @@ -68,7 +68,9 @@ }, { "id": "disabledState", - "name": "Disabled state", + "name": "{i18n:widgets.rpc-state.disabled-state}", + "hint": "{i18n:widgets.rpc-state.disabled-state-hint}", + "stateLabel": "{i18n:widgets.rpc-state.disabled}", "type": "value", "valueType": "BOOLEAN", "defaultValue": false, @@ -77,6 +79,9 @@ { "id": "onState", "name": "On/Off state", + "trueLabel": "{i18n:widgets.rpc-state.on}", + "falseLabel": "{i18n:widgets.rpc-state.off}", + "stateLabel": "{i18n:widgets.rpc-state.on}", "type": "value", "valueType": "BOOLEAN", "defaultValue": false, @@ -84,7 +89,8 @@ }, { "id": "onUpdateState", - "name": "Power On", + "name": "{i18n:widgets.rpc-state.turn-on}", + "hint": "{i18n:widgets.rpc-state.turn-on-hint}", "type": "action", "valueType": "BOOLEAN", "valueToDataType": "CONSTANT", @@ -92,7 +98,8 @@ }, { "id": "offUpdateState", - "name": "Power Off", + "name": "{i18n:widgets.rpc-state.turn-off}", + "hint": "{i18n:widgets.rpc-state.turn-off-hint}", "type": "action", "valueType": "BOOLEAN", "valueToDataType": "CONSTANT", @@ -124,7 +131,7 @@ "id": "levelTitle", "name": "Level title", "type": "string", - "default": "Level", + "default": "{i18n:widgets.battery-level.value}", "disableOnProperty": "showLevelTitle", "fieldClass": "flex" }, From 7d0e5b330b95b7c85023925bc842eee5274cf908 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 8 May 2024 20:00:32 +0300 Subject: [PATCH 009/331] Introduce ResourceSubType for image resources. --- .../main/data/upgrade/3.7.0/schema_update.sql | 32 ++++++++++ .../controller/ControllerConstants.java | 2 + .../server/controller/ImageController.java | 28 ++++++++- .../install/ThingsboardInstallService.java | 4 ++ .../install/SqlDatabaseUpgradeService.java | 3 + .../controller/ImageControllerTest.java | 62 ++++++++++++++++++- .../server/dao/resource/ImageService.java | 5 +- .../server/common/data/ImageExportData.java | 1 + .../server/common/data/ResourceSubType.java | 6 ++ .../server/common/data/TbResourceInfo.java | 3 + .../common/data/TbResourceInfoFilter.java | 1 + .../server/common/util/ProtoUtils.java | 7 +++ common/proto/src/main/proto/queue.proto | 1 + .../server/dao/model/ModelConstants.java | 1 + .../dao/model/sql/TbResourceEntity.java | 9 +++ .../dao/model/sql/TbResourceInfoEntity.java | 9 +++ .../server/dao/resource/BaseImageService.java | 10 ++- .../dao/resource/BaseResourceService.java | 4 +- .../server/dao/resource/TbResourceDao.java | 3 + .../dao/sql/resource/JpaTbResourceDao.java | 5 ++ .../sql/resource/JpaTbResourceInfoDao.java | 7 +++ .../resource/TbResourceInfoRepository.java | 6 +- .../sql/resource/TbResourceRepository.java | 4 ++ .../main/resources/sql/schema-entities.sql | 1 + .../thingsboard/rest/client/RestClient.java | 12 +++- 25 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 application/src/main/data/upgrade/3.7.0/schema_update.sql create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql new file mode 100644 index 0000000000..6ef2074ffe --- /dev/null +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -0,0 +1,32 @@ +-- +-- Copyright ยฉ 2016-2024 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. +-- + +-- UPDATE RESOURCE SUB TYPE START + +DO +$$ + BEGIN + IF NOT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'resource' AND column_name = 'resource_sub_type' + ) THEN + ALTER TABLE resource ADD COLUMN resource_sub_type varchar(32); + UPDATE resource SET resource_sub_type = 'IMAGE' WHERE resource_type = 'IMAGE'; + END IF; + END; +$$; + +-- UPDATE RESOURCE SUB TYPE END diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index d9232cda78..3a39eb5177 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -115,6 +115,8 @@ public class ControllerConstants { protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. "; protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. "; + protected static final String RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION = "A string value representing resource sub-type."; + protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority."; protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title."; diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index c8d24d6081..b81dcd326d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageExportData; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; @@ -64,6 +65,7 @@ import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; @@ -95,7 +97,8 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping("/api/image") public TbResourceInfo uploadImage(@RequestPart MultipartFile file, - @RequestPart(required = false) String title) throws Exception { + @RequestPart(required = false) String title, + @RequestPart(required = false) String imageSubType) throws Exception { SecurityUser user = getCurrentUser(); TbResource image = new TbResource(); image.setTenantId(user.getTenantId()); @@ -108,7 +111,14 @@ public class ImageController extends BaseController { } else { image.setTitle(file.getOriginalFilename()); } + + ResourceSubType subType = ResourceSubType.IMAGE; + if (StringUtils.isNotEmpty(imageSubType)) { + subType = ResourceSubType.valueOf(imageSubType); + } + image.setResourceType(ResourceType.IMAGE); + image.setResourceSubType(subType); ImageDescriptor descriptor = new ImageDescriptor(); descriptor.setMediaType(file.getContentType()); image.setDescriptorValue(descriptor); @@ -193,6 +203,7 @@ public class ImageController extends BaseController { .mediaType(descriptor.getMediaType()) .fileName(imageInfo.getFileName()) .title(imageInfo.getTitle()) + .subType(imageInfo.getResourceSubType().name()) .resourceKey(imageInfo.getResourceKey()) .isPublic(imageInfo.isPublic()) .publicResourceKey(imageInfo.getPublicResourceKey()) @@ -214,6 +225,11 @@ public class ImageController extends BaseController { } else { image.setTitle(imageData.getFileName()); } + if (StringUtils.isNotEmpty(imageData.getSubType())) { + image.setResourceSubType(ResourceSubType.valueOf(imageData.getSubType())); + } else { + image.setResourceSubType(ResourceSubType.IMAGE); + } image.setResourceType(ResourceType.IMAGE); image.setResourceKey(imageData.getResourceKey()); image.setPublic(imageData.isPublic()); @@ -250,6 +266,8 @@ public class ImageController extends BaseController { @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"IMAGE", "IOT_SVG"})) + @RequestParam(required = false) String imageSubType, @Parameter(description = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION) @RequestParam(required = false) boolean includeSystemImages, @Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION) @@ -261,10 +279,14 @@ public class ImageController extends BaseController { // PE: generic permission PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); TenantId tenantId = getTenantId(); + ResourceSubType subType = ResourceSubType.IMAGE; + if (StringUtils.isNotEmpty(imageSubType)) { + subType = ResourceSubType.valueOf(imageSubType); + } if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) { - return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink)); + return checkNotNull(imageService.getImagesByTenantId(tenantId, subType, pageLink)); } else { - return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink)); + return checkNotNull(imageService.getAllImagesByTenantId(tenantId, subType, pageLink)); } } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 33e2ddee01..eba35157c9 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -137,6 +137,10 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists(); systemDataLoaderService.updateDefaultNotificationConfigs(false); systemDataLoaderService.updateJwtSettings(); + break; + case "3.7.0": + log.info("Upgrading ThingsBoard from version 3.7.0 to 3.7.1 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.7.0"); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 86d922c305..000c0536cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -121,6 +121,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService case "3.6.4": updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null); break; + case "3.7.0": + updateSchema("3.7.0", 3007000, "3.7.1", 3007001, null); + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index e272a40857..53b36c955b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -23,11 +23,14 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockPart; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageExportData; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.page.PageData; @@ -35,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.sql.resource.TbResourceRepository; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; @@ -53,6 +57,7 @@ public class ImageControllerTest extends AbstractControllerTest { @Before public void beforeEach() throws Exception { + resourceRepository.deleteAll(); loginTenantAdmin(); } @@ -66,6 +71,8 @@ public class ImageControllerTest extends AbstractControllerTest { String filename = "my_png_image.png"; TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE); + assertThat(imageInfo.getTitle()).isEqualTo(filename); assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE); assertThat(imageInfo.getResourceKey()).isEqualTo(filename); @@ -85,6 +92,8 @@ public class ImageControllerTest extends AbstractControllerTest { String filename = "my_jpeg_image.jpg"; TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE); + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); checkJpegImageDescriptor(imageDescriptor); @@ -97,6 +106,22 @@ public class ImageControllerTest extends AbstractControllerTest { String filename = "my_svg_image.svg"; TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE); + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE); + + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); + checkSvgImageDescriptor(imageDescriptor); + + assertThat(downloadImage("tenant", filename)).containsExactly(SVG_IMAGE); + assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize()); + } + + @Test + public void testUploadIoTSvgImage() throws Exception { + String filename = "my_iot_svg_image.svg"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.IOT_SVG.name(), filename, "image/svg+xml", SVG_IMAGE); + + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IOT_SVG); + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); checkSvgImageDescriptor(imageDescriptor); @@ -175,6 +200,7 @@ public class ImageControllerTest extends AbstractControllerTest { assertThat(exportData.getMediaType()).isEqualTo("image/png"); assertThat(exportData.getFileName()).isEqualTo(filename); assertThat(exportData.getTitle()).isEqualTo(filename); + assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name()); assertThat(exportData.getResourceKey()).isEqualTo(filename); assertThat(exportData.getData()).isEqualTo(Base64.getEncoder().encodeToString(PNG_IMAGE)); assertThat(exportData.isPublic()).isTrue(); @@ -184,6 +210,7 @@ public class ImageControllerTest extends AbstractControllerTest { TbResourceInfo importedImageInfo = doPut("/api/image/import", exportData, TbResourceInfo.class); assertThat(importedImageInfo.getTitle()).isEqualTo(filename); + assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name()); assertThat(importedImageInfo.getResourceKey()).isEqualTo(filename); assertThat(importedImageInfo.getFileName()).isEqualTo(filename); assertThat(importedImageInfo.isPublic()).isTrue(); @@ -198,20 +225,37 @@ public class ImageControllerTest extends AbstractControllerTest { String systemImageName = "my_system_png_image.png"; TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE); + String systemIotSvgName = "my_system_iot_svg_image.svg"; + TbResourceInfo systemIotSvg = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.IOT_SVG.name(), systemIotSvgName, "image/svg+xml", SVG_IMAGE); + loginTenantAdmin(); String tenantImageName = "my_jpeg_image.jpg"; TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE); + String tenantIotSvgName = "my_iot_svg_image.svg"; + TbResourceInfo tenantIotSvg = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.IOT_SVG.name(), tenantIotSvgName, "image/svg+xml", SVG_IMAGE); + List tenantImages = getImages(null, false, 10); assertThat(tenantImages).containsOnly(tenantImage); + List tenantIotSvgs = getImages(null, ResourceSubType.IOT_SVG.name(), false, 10); + assertThat(tenantIotSvgs).containsOnly(tenantIotSvg); + List allImages = getImages(null, true, 10); assertThat(allImages).containsOnly(tenantImage, systemImage); + List allIotSvgs = getImages(null, ResourceSubType.IOT_SVG.name(), true, 10); + assertThat(allIotSvgs).containsOnly(tenantIotSvg, systemIotSvg); + assertThat(getImages("png", true, 10)) .containsOnly(systemImage); assertThat(getImages("jpg", true, 10)) .containsOnly(tenantImage); + + assertThat(getImages("my_system_iot", ResourceSubType.IOT_SVG.name(), true, 10)) + .containsOnly(systemIotSvg); + assertThat(getImages("my_iot_svg", ResourceSubType.IOT_SVG.name(),true, 10)) + .containsOnly(tenantIotSvg); } @Test @@ -312,7 +356,15 @@ public class ImageControllerTest extends AbstractControllerTest { } private List getImages(String searchText, boolean includeSystemImages, int limit) throws Exception { - PageData images = doGetTypedWithPageLink("/api/images?includeSystemImages=" + includeSystemImages + "&", new TypeReference<>() {}, new PageLink(limit, 0, searchText)); + return this.getImages(searchText, null, includeSystemImages, limit); + } + + private List getImages(String searchText, String imageSubType, boolean includeSystemImages, int limit) throws Exception { + var url = "/api/images?includeSystemImages=" + includeSystemImages + "&"; + if (StringUtils.isNotEmpty(imageSubType)) { + url += "imageSubType=" + imageSubType+ "&"; + } + PageData images = doGetTypedWithPageLink(url, new TypeReference<>() {}, new PageLink(limit, 0, searchText)); return images.getData(); } @@ -332,8 +384,16 @@ public class ImageControllerTest extends AbstractControllerTest { } private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception { + return this.uploadImage(httpMethod, url, null, filename, mediaType, content); + } + + private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception { MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content); var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file); + if (StringUtils.isNotEmpty(subType)) { + var imageSubTypePart = new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8)); + request.part(imageSubTypePart); + } setJwtToken(request); return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java index 9a65fcfdae..e1253ae568 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -36,9 +37,9 @@ public interface ImageService { TbResourceInfo getPublicImageInfoByKey(String publicResourceKey); - PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink); + PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink); - PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink); + PageData getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink); byte[] getImageData(TenantId tenantId, TbResourceId imageId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java index b407563d24..3be7b6f92d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java @@ -33,6 +33,7 @@ public class ImageExportData { private String mediaType; private String fileName; private String title; + private String subType; private String resourceKey; private boolean isPublic; private String publicResourceKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java new file mode 100644 index 0000000000..e7e8b0747a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java @@ -0,0 +1,6 @@ +package org.thingsboard.server.common.data; + +public enum ResourceSubType { + IMAGE, + IOT_SVG +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index f1318c2257..8537fe68ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -46,6 +46,8 @@ public class TbResourceInfo extends BaseData implements HasName, H private String title; @Schema(description = "Resource type.", example = "LWM2M_MODEL", accessMode = Schema.AccessMode.READ_ONLY) private ResourceType resourceType; + @Schema(description = "Resource sub type.", example = "IOT_SVG", accessMode = Schema.AccessMode.READ_ONLY) + private ResourceSubType resourceSubType; @NoXss @Length(fieldName = "resourceKey") @Schema(description = "Resource key.", example = "19_1.0", accessMode = Schema.AccessMode.READ_ONLY) @@ -78,6 +80,7 @@ public class TbResourceInfo extends BaseData implements HasName, H this.tenantId = resourceInfo.tenantId; this.title = resourceInfo.title; this.resourceType = resourceInfo.resourceType; + this.resourceSubType = resourceInfo.resourceSubType; this.resourceKey = resourceInfo.resourceKey; this.searchText = resourceInfo.searchText; this.isPublic = resourceInfo.isPublic; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java index 87d9b69749..60583dbb96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java @@ -27,5 +27,6 @@ public class TbResourceInfoFilter { private TenantId tenantId; private Set resourceTypes; + private Set resourceSubTypes; } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index e89ce5634c..0ddc309b46 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; @@ -827,6 +828,9 @@ public class ProtoUtils { if (isNotNull(resource.getPreview())) { builder.setPreview(ByteString.copyFrom(resource.getPreview())); } + if (isNotNull(resource.getResourceSubType())) { + builder.setResourceSubType(resource.getResourceSubType().name()); + } return builder.build(); } @@ -858,6 +862,9 @@ public class ProtoUtils { if (proto.hasPreview()) { resource.setPreview(proto.getPreview().toByteArray()); } + if (proto.hasResourceSubType()) { + resource.setResourceSubType(ResourceSubType.valueOf(proto.getResourceSubType())); + } return resource; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 7b2aa2fe1b..afc157e476 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -286,6 +286,7 @@ message TbResourceProto { optional int64 externalIdLSB = 16; optional bytes data = 17; optional bytes preview = 18; + optional string resourceSubType = 19; } message ApiUsageStateProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 8f0502efbf..fdaf7fa96c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -498,6 +498,7 @@ public class ModelConstants { public static final String RESOURCE_TABLE_NAME = "resource"; public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN; public static final String RESOURCE_TYPE_COLUMN = "resource_type"; + public static final String RESOURCE_SUB_TYPE_COLUMN = "resource_sub_type"; public static final String RESOURCE_KEY_COLUMN = "resource_key"; public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY; public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index f507a8661f..ef12d656b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -20,6 +20,7 @@ import jakarta.persistence.Convert; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; @@ -42,6 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; @@ -63,6 +65,9 @@ public class TbResourceEntity extends BaseSqlEntity { @Column(name = RESOURCE_TYPE_COLUMN) private String resourceType; + @Column(name = RESOURCE_SUB_TYPE_COLUMN) + private String resourceSubType; + @Column(name = RESOURCE_KEY_COLUMN) private String resourceKey; @@ -107,6 +112,9 @@ public class TbResourceEntity extends BaseSqlEntity { } this.title = resource.getTitle(); this.resourceType = resource.getResourceType().name(); + if (resource.getResourceSubType() != null) { + this.resourceSubType = resource.getResourceSubType().name(); + } this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); this.fileName = resource.getFileName(); @@ -126,6 +134,7 @@ public class TbResourceEntity extends BaseSqlEntity { resource.setTenantId(TenantId.fromUUID(tenantId)); resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); + resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); resource.setFileName(fileName); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 8f525916bf..35394d5baa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Convert; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.id.TbResourceId; @@ -39,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; @@ -60,6 +62,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen @Column(name = RESOURCE_TYPE_COLUMN) private String resourceType; + @Column(name = RESOURCE_SUB_TYPE_COLUMN) + private String resourceSubType; + @Column(name = RESOURCE_KEY_COLUMN) private String resourceKey; @@ -96,6 +101,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.tenantId = resource.getTenantId().getId(); this.title = resource.getTitle(); this.resourceType = resource.getResourceType().name(); + if (resource.getResourceSubType() != null) { + this.resourceSubType = resource.getResourceSubType().name(); + } this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); this.etag = resource.getEtag(); @@ -113,6 +121,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setTenantId(TenantId.fromUUID(tenantId)); resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); + resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); resource.setEtag(etag); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 953037a864..28d00f8b5d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.ImageDescriptor; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; @@ -140,6 +141,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic if (image.getId() == null) { image.setResourceKey(getUniqueKey(image.getTenantId(), StringUtils.defaultIfEmpty(image.getResourceKey(), image.getFileName()))); } + if (image.getResourceSubType() == null) { + image.setResourceSubType(ResourceSubType.IMAGE); + } resourceValidator.validate(image, TbResourceInfo::getTenantId); ImageDescriptor descriptor = image.getDescriptor(ImageDescriptor.class); @@ -220,21 +224,23 @@ public class BaseImageService extends BaseResourceService implements ImageServic } @Override - public PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) { log.trace("Executing getImagesByTenantId [{}]", tenantId); TbResourceInfoFilter filter = TbResourceInfoFilter.builder() .tenantId(tenantId) .resourceTypes(Set.of(ResourceType.IMAGE)) + .resourceSubTypes(Set.of(imageSubType)) .build(); return findTenantResourcesByTenantId(filter, pageLink); } @Override - public PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) { log.trace("Executing getAllImagesByTenantId [{}]", tenantId); TbResourceInfoFilter filter = TbResourceInfoFilter.builder() .tenantId(tenantId) .resourceTypes(Set.of(ResourceType.IMAGE)) + .resourceSubTypes(Set.of(imageSubType)) .build(); return findAllTenantResourcesByTenantId(filter, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 723d0e8861..93cc662472 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -184,7 +184,7 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) { log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, objectIds, null); } @Override @@ -198,7 +198,7 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) { log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index b776b49cc9..c236a4a5d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.resource; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; @@ -35,10 +36,12 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao, PageData findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, PageLink pageLink); List findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, String[] objectIds, String searchText); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index cb2ab5aa42..f8927aaa81 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; @@ -68,11 +69,13 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, PageLink pageLink) { return DaoUtil.toPageData(resourceRepository.findResourcesPage( tenantId.getId(), TenantId.SYS_TENANT_ID.getId(), resourceType.name(), + resourceSubType != null ? resourceSubType.name() : null, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink) )); @@ -80,6 +83,7 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, String[] objectIds, String searchText) { return objectIds == null ? @@ -87,6 +91,7 @@ public class JpaTbResourceDao extends JpaAbstractDao resourceSubTypes = filter.getResourceSubTypes(); return DaoUtil.toPageData(resourceInfoRepository .findAllTenantResourcesByTenantId( filter.getTenantId().getId(), TenantId.NULL_UUID, resourceTypes.stream().map(Enum::name).collect(Collectors.toList()), + CollectionsUtil.isEmpty(resourceSubTypes) ? null : + resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); } @@ -77,10 +81,13 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao resourceSubTypes = filter.getResourceSubTypes(); return DaoUtil.toPageData(resourceInfoRepository .findTenantResourcesByTenantId( filter.getTenantId().getId(), resourceTypes.stream().map(Enum::name).collect(Collectors.toList()), + CollectionsUtil.isEmpty(resourceSubTypes) ? null : + resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index c092ea3292..00c51b50a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -37,19 +37,23 @@ public interface TbResourceInfoRepository extends JpaRepository findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, @Param("systemTenantId") UUID systemTenantId, @Param("resourceTypes") List resourceTypes, + @Param("resourceSubTypes") List resourceSubTypes, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " + "ri.tenantId = :tenantId " + "AND ri.resourceType IN :resourceTypes " + + "AND (:resourceSubTypes IS NULL OR ri.resourceSubType IN :resourceSubTypes) " + "AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)") Page findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, @Param("resourceTypes") List resourceTypes, + @Param("resourceSubTypes") List resourceSubTypes, @Param("searchText") String searchText, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index f5a831ff64..ce2e6b890b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -34,6 +34,7 @@ public interface TbResourceRepository extends JpaRepository findResources(@Param("tenantId") UUID tenantId, @Param("systemAdminId") UUID sysAdminId, @Param("resourceType") String resourceType, + @Param("resourceSubType") String resourceSubType, @Param("searchText") String searchText); @Query("SELECT tr FROM TbResourceEntity tr " + diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9c1841240b..375cd9f689 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -715,6 +715,7 @@ CREATE TABLE IF NOT EXISTS resource ( tenant_id uuid NOT NULL, title varchar(255) NOT NULL, resource_type varchar(32) NOT NULL, + resource_sub_type varchar(32), resource_key varchar(255) NOT NULL, search_text varchar(255), file_name varchar(255) NOT NULL, diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 8783770be3..39d408464f 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.ImageExportData; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.SystemInfo; @@ -3603,10 +3604,19 @@ public class RestClient implements Closeable { } public PageData getImages(PageLink pageLink, boolean includeSystemImages) { + return this.getImages(pageLink, null, includeSystemImages); + } + + public PageData getImages(PageLink pageLink, ResourceSubType imageSubType, boolean includeSystemImages) { Map params = new HashMap<>(); + var url = baseURL + "/api/images?includeSystemImages={includeSystemImages}&"; addPageLinkToParam(params, pageLink); params.put("includeSystemImages", String.valueOf(includeSystemImages)); - return restTemplate.exchange(baseURL + "/api/images?includeSystemImages={includeSystemImages}&" + getUrlParams(pageLink), + if (imageSubType != null) { + url += "imageSubType={imageSubType}&"; + params.put("imageSubType", imageSubType.name()); + } + return restTemplate.exchange(url + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() {}, From 69e1455e4089c1a7266c6cbc81de06141cfd9d00 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 8 May 2024 20:01:23 +0300 Subject: [PATCH 010/331] Update license header --- .../server/common/data/ResourceSubType.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java index e7e8b0747a..13203f908f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java @@ -1,3 +1,18 @@ +/** + * Copyright ยฉ 2016-2024 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.common.data; public enum ResourceSubType { From a8647c71896b946173077ef6aa5320b05feda1eb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 9 May 2024 09:55:42 +0300 Subject: [PATCH 011/331] Fix tests --- .../server/LwM2mVersionedModelProvider.java | 4 +-- .../lwm2m/server/client/LwM2mClient.java | 30 ++++++++++--------- .../server/store/util/LwM2MClientSerDes.java | 3 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 78f76adc1d..ab52a024da 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -108,9 +108,9 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public ObjectModel getObjectModel(int objectId) { LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); - String version = lwM2mClient.getSupportedObjectVersion(objectId).toString(); + var version = lwM2mClient.getSupportedObjectVersion(objectId); if (version != null) { - return this.getObjectModelDynamic(objectId, version); + return this.getObjectModelDynamic(objectId, version.toString()); } return null; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index e267561cd2..908b16a1db 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -211,11 +211,11 @@ public class LwM2mClient { } private LwM2m.Version getObjectIDVerFromDeviceProfile(DeviceProfile deviceProfile) { - String defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration)deviceProfile + String defaultObjectIdVer = deviceProfile != null ? ((Lwm2mDeviceProfileTransportConfiguration)deviceProfile .getProfileData() .getTransportConfiguration()) .getClientLwM2mSettings() - .getDefaultObjectIDVer(); + .getDefaultObjectIDVer() : null; return new Version(defaultObjectIdVer == null ? LWM2M_OBJECT_VERSION_DEFAULT : defaultObjectIdVer); } @@ -454,19 +454,21 @@ public class LwM2mClient { private void setSupportedClientObjects(){ this.supportedClientObjects = new ConcurrentHashMap<>(); for (Link link: this.registration.getSortedObjectLinks()) { - MixedLwM2mLink mixedLwM2mLink = (MixedLwM2mLink)link; - if(!mixedLwM2mLink.getPath().isRoot()){ - LwM2mPath lwM2mPath = mixedLwM2mLink.getPath(); - if (lwM2mPath.isObject()) { - LwM2m.Version ver; - if (mixedLwM2mLink.getAttributes().get("ver")!= null) { - ver = (Version) mixedLwM2mLink.getAttributes().get("ver").getValue(); - } else { - ver = getDefaultObjectIDVer(); + if (link instanceof MixedLwM2mLink) { + MixedLwM2mLink mixedLwM2mLink = (MixedLwM2mLink) link; + if (!mixedLwM2mLink.getPath().isRoot()) { + LwM2mPath lwM2mPath = mixedLwM2mLink.getPath(); + if (lwM2mPath.isObject()) { + LwM2m.Version ver; + if (mixedLwM2mLink.getAttributes().get("ver") != null) { + ver = (Version) mixedLwM2mLink.getAttributes().get("ver").getValue(); + } else { + ver = getDefaultObjectIDVer(); + } + this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); + } else if (this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null) { + this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); } - this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); - } else if (this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ - this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 21cdc356a4..ece90b2116 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -23,6 +23,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.protobuf.util.JsonFormat; import lombok.SneakyThrows; +import org.eclipse.leshan.core.LwM2m; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mNodeException; @@ -345,7 +346,7 @@ public class LwM2MClientSerDes { if (defaultObjectIDVer != null) { Field defaultObjectIDVerField = lwM2mClientClass.getDeclaredField("defaultObjectIDVer"); defaultObjectIDVerField.setAccessible(true); - defaultObjectIDVerField.set(lwM2mClient, defaultObjectIDVer.getAsString()); + defaultObjectIDVerField.set(lwM2mClient, new LwM2m.Version(defaultObjectIDVer.getAsString())); } JsonElement registration = o.get("registration"); From 25d733324ff640a32f2dd7c24a8a4aedfdfdc559 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 9 May 2024 11:14:55 +0300 Subject: [PATCH 012/331] Update test --- .../org/thingsboard/server/controller/ImageControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index 53b36c955b..cf43f54df9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -57,7 +57,6 @@ public class ImageControllerTest extends AbstractControllerTest { @Before public void beforeEach() throws Exception { - resourceRepository.deleteAll(); loginTenantAdmin(); } From 667536aba76d2af3f3e66c036fa309eb81ac6945 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 9 May 2024 14:49:05 +0300 Subject: [PATCH 013/331] Fix install script --- .../thingsboard/server/install/ThingsboardInstallService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index eba35157c9..bab9e1fbc9 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -137,7 +137,6 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists(); systemDataLoaderService.updateDefaultNotificationConfigs(false); systemDataLoaderService.updateJwtSettings(); - break; case "3.7.0": log.info("Upgrading ThingsBoard from version 3.7.0 to 3.7.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.7.0"); From ecb11b6a3ad1d9dec7d8bcafc2ce8bab7cdd710e Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 9 May 2024 18:56:31 +0300 Subject: [PATCH 014/331] UI: SCADA symbol edit page. --- ui-ngx/src/app/app.module.ts | 21 +++- ui-ngx/src/app/core/http/image.service.ts | 39 ++++++- ui-ngx/src/app/core/services/menu.service.ts | 14 +++ ui-ngx/src/app/core/utils.ts | 7 ++ .../components/details-panel.component.html | 2 +- .../components/details-panel.component.ts | 5 + .../widget/lib/svg/iot-svg.models.ts | 50 +++++++++ .../home/pages/admin/admin-routing.module.ts | 67 +++++++++++- .../modules/home/pages/home-pages.module.ts | 4 +- .../scada-symbol/scada-symbol.component.html | 42 ++++++++ .../scada-symbol/scada-symbol.component.scss | 35 ++++++ .../scada-symbol/scada-symbol.component.ts | 102 ++++++++++++++++++ .../pages/scada-symbol/scada-symbol.models.ts | 22 ++++ .../pages/scada-symbol/scada-symbol.module.ts | 34 ++++++ .../components/image-input.component.html | 2 +- .../components/image-input.component.ts | 38 +++++-- .../image/gallery-image-input.component.ts | 15 ++- .../image/image-dialog.component.ts | 1 + .../image/image-gallery-dialog.component.html | 1 + .../image/image-gallery-dialog.component.ts | 23 ++-- .../image/image-gallery.component.html | 54 +++++----- .../image/image-gallery.component.ts | 71 ++++++++---- .../components/image/images-datasource.ts | 10 +- .../multiple-gallery-image-input.component.ts | 15 ++- .../image/upload-image-dialog.component.html | 6 +- .../image/upload-image-dialog.component.ts | 9 +- .../src/app/shared/models/resource.models.ts | 7 ++ .../assets/locale/locale.constant-en_US.json | 20 ++++ 28 files changed, 610 insertions(+), 106 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts diff --git a/ui-ngx/src/app/app.module.ts b/ui-ngx/src/app/app.module.ts index e18c909c9a..1ebd87c119 100644 --- a/ui-ngx/src/app/app.module.ts +++ b/ui-ngx/src/app/app.module.ts @@ -27,6 +27,23 @@ import { AppComponent } from './app.component'; import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module'; import { RouterModule, Routes } from '@angular/router'; +import { DefaultUrlSerializer, UrlSerializer, UrlTree } from '@angular/router'; + +export default class TbUrlSerializer implements UrlSerializer { + private _defaultUrlSerializer: DefaultUrlSerializer = new DefaultUrlSerializer(); + + parse(url: string): UrlTree { + // Encode parentheses + url = url.replace(/\(/g, '%28').replace(/\)/g, '%29'); + // Use the default serializer. + return this._defaultUrlSerializer.parse(url) + } + + serialize(tree: UrlTree): string { + return this._defaultUrlSerializer.serialize(tree).replace(/%28/g, '(').replace(/%29/g, ')'); + } +} + const routes: Routes = [ { path: '**', redirectTo: 'home' @@ -55,7 +72,9 @@ export class PageNotFoundRoutingModule { } DashboardRoutingModule, PageNotFoundRoutingModule ], - providers: [], + providers: [ + { provide: UrlSerializer, useClass: TbUrlSerializer } + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts index 2339801e2b..eb15b658e5 100644 --- a/ui-ngx/src/app/core/http/image.service.ts +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -25,11 +25,11 @@ import { ImageResourceInfo, imageResourceType, ImageResourceType, - IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix + IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix, ResourceSubType } from '@shared/models/resource.models'; import { catchError, map, switchMap } from 'rxjs/operators'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { blobToBase64 } from '@core/utils'; +import { blobToBase64, blobToText } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; @Injectable({ @@ -46,13 +46,14 @@ export class ImageService { ) { } - public uploadImage(file: File, title: string, config?: RequestConfig): Observable { + public uploadImage(file: File, title: string, imageSubType: ResourceSubType, config?: RequestConfig): Observable { if (!config) { config = {}; } const formData = new FormData(); formData.append('file', file); formData.append('title', title); + formData.append('imageSubType', imageSubType); return this.http.post('/api/image', formData, defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); } @@ -81,9 +82,10 @@ export class ImageService { imageInfo, defaultHttpOptionsFromConfig(config)); } - public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable> { + public getImages(pageLink: PageLink, imageSubType: ResourceSubType, + includeSystemImages = false, config?: RequestConfig): Observable> { return this.http.get>( - `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`, + `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`, defaultHttpOptionsFromConfig(config)); } @@ -130,6 +132,33 @@ export class ImageService { ); } + public getImageString(imageUrl: string): Observable { + imageUrl = removeTbImagePrefix(imageUrl); + let request: ReplaySubject; + if (this.imagesLoading[imageUrl]) { + request = this.imagesLoading[imageUrl]; + } else { + request = new ReplaySubject(1); + this.imagesLoading[imageUrl] = request; + const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); + this.http.get(imageUrl, {...options, ...{ responseType: 'blob' } }).subscribe({ + next: (value) => { + request.next(value); + request.complete(); + }, + error: err => { + request.error(err); + }, + complete: () => { + delete this.imagesLoading[imageUrl]; + } + }); + } + return request.pipe( + switchMap(val => blobToText(val)) + ); + } + public resolveImageUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable { imageUrl = removeTbImagePrefix(imageUrl); if (isImageResourceUrl(imageUrl)) { diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 1f2d76a8b8..256c1031ea 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -150,6 +150,13 @@ export class MenuService { path: '/resources/images', icon: 'filter' }, + { + id: 'scada_symbols', + name: 'scada.symbols', + type: 'link', + path: '/resources/scada-symbols', + icon: 'precision_manufacturing' + }, { id: 'resources_library', name: 'resource.resources-library', @@ -541,6 +548,13 @@ export class MenuService { path: '/resources/images', icon: 'filter' }, + { + id: 'scada_symbols', + name: 'scada.symbols', + type: 'link', + path: '/resources/scada-symbols', + icon: 'precision_manufacturing' + }, { id: 'resources_library', name: 'resource.resources-library', diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index dc9ed5004b..c9aebfc39b 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -217,6 +217,13 @@ export const blobToBase64 = (blob: Blob): Observable => from(new Promise } )); +export const blobToText = (blob: Blob): Observable => from(new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.readAsText(blob); + } +)); + const scrollRegex = /(auto|scroll)/; function parentNodes(node: Node, nodes: Node[]): Node[] { diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index d3e50ca1d7..2483a5b5e3 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -40,7 +40,7 @@ search -
diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.ts b/ui-ngx/src/app/modules/home/components/details-panel.component.ts index bdcfbd636b..3b536c809e 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.ts @@ -28,6 +28,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { UntypedFormGroup } from '@angular/forms'; import { Subscription } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-details-panel', @@ -44,6 +45,10 @@ export class DetailsPanelComponent extends PageComponent implements OnDestroy { @Input() isShowSearch = false; @Input() backgroundColor = '#FFF'; + @Input() + @coerceBoolean() + showCloseDetails = true; + private theFormValue: UntypedFormGroup; private formSubscription: Subscription = null; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index 4a19709228..8e369cf580 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -40,6 +40,7 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetAction, WidgetActionType } from '@shared/models/widget.models'; +import { ResizeObserver } from '@juggle/resize-observer'; export interface IotSvgApi { formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; @@ -248,6 +249,55 @@ export type IotSvgObjectSettings = {[id: string]: any}; const parseError = (ctx: WidgetContext, err: any): string => ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; +export class IotSvgEditObject { + + private svgShape: Svg; + private box: Box; + + private shapeResize$: ResizeObserver; + constructor(private rootElement: HTMLElement) { + this.shapeResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.shapeResize$.observe(this.rootElement); + } + + public setContent(svgContent: string) { + if (this.svgShape) { + this.svgShape.remove(); + } + const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + this.svgShape = SVG().svg(doc.documentElement.innerHTML); + this.svgShape.node.style.overflow = 'visible'; + this.svgShape.node.style['user-select'] = 'none'; + this.box = this.svgShape.bbox(); + this.svgShape.size(this.box.width, this.box.height); + this.svgShape.addTo(this.rootElement); + this.resize(); + } + + public destroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + } + + private resize() { + if (this.svgShape) { + const targetWidth = this.rootElement.getBoundingClientRect().width; + const targetHeight = this.rootElement.getBoundingClientRect().height; + let scale: number; + if (targetWidth < targetHeight) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + this.svgShape.node.style.transform = `scale(${scale})`; + } + } + +} + export class IotSvgObject { private metadata: IotSvgMetadata; diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 63fb9a14de..e292f3cfbb 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Injectable, NgModule } from '@angular/core'; -import { Resolve, RouterModule, Routes } from '@angular/router'; +import { inject, Injectable, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @@ -23,7 +23,7 @@ import { Authority } from '@shared/models/authority.enum'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable } from 'rxjs'; import { OAuth2Service } from '@core/http/oauth2.service'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; @@ -31,7 +31,7 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/resources-library-table-config.resolve'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; -import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; @@ -40,6 +40,10 @@ import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing. import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; +import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/models/resource.models'; +import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; +import { ImageService } from '@core/http/image.service'; +import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol.models'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -52,6 +56,22 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve { } } +export const scadaSymbolResolver: ResolveFn = + (route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + imageService = inject(ImageService)) => { + const type: ImageResourceType = route.params.type; + const key = decodeURIComponent(route.params.key); + return forkJoin({ + imageResource: imageService.getImageInfo(type, key), + svgContent: imageService.getImageString(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`) + }); +}; + +export const scadaSymbolBreadcumbLabelFunction: BreadCrumbLabelFunction + = ((route, translate, component) => + component.symbolData?.imageResource?.title); + const routes: Routes = [ { path: 'resources', @@ -87,8 +107,45 @@ const routes: Routes = [ data: { auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'image.gallery', - }, + imageSubType: ResourceSubType.IMAGE + } + } + ] + }, + { + path: 'scada-symbols', + data: { + breadcrumb: { + label: 'scada.symbols', + icon: 'precision_manufacturing' } + }, + children: [ + { + path: '', + component: ImageGalleryComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'scada.symbols', + imageSubType: ResourceSubType.IOT_SVG + } + }, + { + path: ':type/:key', + component: ScadaSymbolComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: scadaSymbolBreadcumbLabelFunction, + icon: 'precision_manufacturing' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'scada.symbol' + }, + resolve: { + symbolData: scadaSymbolResolver + } + }, ] }, { diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 63a3aace21..0b1a70996e 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -43,6 +43,7 @@ import { EntitiesModule } from '@home/pages/entities/entities.module'; import { FeaturesModule } from '@home/pages/features/features.module'; import { NotificationModule } from '@home/pages/notification/notification.module'; import { AccountModule } from '@home/pages/account/account.module'; +import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module'; @NgModule({ exports: [ @@ -72,7 +73,8 @@ import { AccountModule } from '@home/pages/account/account.module'; OtaUpdateModule, UserModule, VcModule, - AccountModule + AccountModule, + ScadaSymbolModule ] }) export class HomePagesModule { } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html new file mode 100644 index 0000000000..cd366abbde --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -0,0 +1,42 @@ + +
+
+
+ + + +
+ {{ metadata | json }} +
+
+
+ +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss new file mode 100644 index 0000000000..783e2d842b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol { + width: 100%; + height: 100%; + .tb-scada-symbol-details-drawer { + width: 50%; + } + .tb-scada-symbol-content { + flex: 1; + min-width: 0; + min-height: 0; + background: #fff; + .tb-scada-symbol-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts new file mode 100644 index 0000000000..d75ac137e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -0,0 +1,102 @@ +/// +/// Copyright ยฉ 2016-2024 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 { + AfterViewInit, + Component, + ElementRef, + HostBinding, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute, Router } from '@angular/router'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol.models'; +import { + IotSvgEditObject, + IotSvgMetadata, + parseIotSvgMetadataFromContent +} from '@home/components/widget/lib/svg/iot-svg.models'; + +@Component({ + selector: 'tb-scada-symbol', + templateUrl: './scada-symbol.component.html', + styleUrls: ['./scada-symbol.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit { + + @HostBinding('style.width') width = '100%'; + @HostBinding('style.height') height = '100%'; + + @ViewChild('iotSvgShape', {static: false}) + iotSvgShape: ElementRef; + + symbolData: ScadaSymbolData; + + iotSvgObjectEdit: IotSvgEditObject; + metadata: IotSvgMetadata; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + private router: Router, + private route: ActivatedRoute) { + super(store); + this.route.data.pipe( + takeUntil(this.destroy$) + ).subscribe( + () => { + this.reset(); + this.init(); + } + ); + } + + ngOnInit(): void { + } + + ngAfterViewInit() { + this.iotSvgObjectEdit = new IotSvgEditObject(this.iotSvgShape.nativeElement); + if (this.symbolData) { + this.iotSvgObjectEdit.setContent(this.symbolData.svgContent); + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + private reset(): void { + } + + private init() { + this.symbolData = this.route.snapshot.data.symbolData; + this.metadata = parseIotSvgMetadataFromContent(this.symbolData.svgContent); + if (this.iotSvgObjectEdit) { + this.iotSvgObjectEdit.setContent(this.symbolData.svgContent); + } + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts new file mode 100644 index 0000000000..32b05f2265 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts @@ -0,0 +1,22 @@ +/// +/// Copyright ยฉ 2016-2024 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 { ImageResourceInfo } from '@shared/models/resource.models'; + +export interface ScadaSymbolData { + imageResource: ImageResourceInfo; + svgContent: string; +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts new file mode 100644 index 0000000000..2ed9a0bb4c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts @@ -0,0 +1,34 @@ +/// +/// Copyright ยฉ 2016-2024 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; + +@NgModule({ + declarations: + [ + ScadaSymbolComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule + ] +}) +export class ScadaSymbolModule { } diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html index 6a657f70b1..3b7130b634 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.html +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -47,7 +47,7 @@ - +
diff --git a/ui-ngx/src/app/shared/components/image-input.component.ts b/ui-ngx/src/app/shared/components/image-input.component.ts index acbff9844f..5bf98c65a9 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image-input.component.ts @@ -53,6 +53,9 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; }) export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { + @Input() + accept = 'image/*'; + @Input() label: string; @@ -85,6 +88,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @Input() inputId = this.utils.guid(); + @Input() + allowedExtensions: string; + @Input() @coerceBoolean() processImageApiLink = false; @@ -141,17 +147,19 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, ); return false; } - const reader = new FileReader(); - reader.onload = (_loadEvent) => { - if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { - this.imageUrl = reader.result; - this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); - this.file = file; - this.fileName = fileName; - this.updateModel(); - } - }; - reader.readAsDataURL(file); + if (this.filterFile(flowFile)) { + const reader = new FileReader(); + reader.onload = (_loadEvent) => { + if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { + this.imageUrl = reader.result; + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + this.file = file; + this.fileName = fileName; + this.updateModel(); + } + }; + reader.readAsDataURL(file); + } } }); } @@ -198,6 +206,14 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, this.fileNameChanged.emit(this.fileName); } + private filterFile(file: flowjs.FlowFile): boolean { + if (this.allowedExtensions) { + return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1; + } else { + return true; + } + } + clearImage() { this.imageUrl = null; this.safeImageUrl = null; diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts index 9b81a0eaba..fd44b6cfec 100644 --- a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts @@ -26,11 +26,15 @@ import { isBase64DataImageUrl, isImageResourceUrl, prependTbImagePrefix, - removeTbImagePrefix + removeTbImagePrefix, + ResourceSubType } from '@shared/models/resource.models'; import { ImageService } from '@core/http/image.service'; import { MatDialog } from '@angular/material/dialog'; -import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component'; +import { + ImageGalleryDialogComponent, + ImageGalleryDialogData +} from '@shared/components/image/image-gallery-dialog.component'; export enum ImageLinkType { none = 'none', @@ -188,11 +192,14 @@ export class GalleryImageInputComponent extends PageComponent implements OnInit, if ($event) { $event.stopPropagation(); } - this.dialog.open(ImageGalleryDialogComponent, { autoFocus: false, disableClose: false, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + imageSubType: ResourceSubType.IMAGE + } }).afterClosed().subscribe((image) => { if (image) { this.linkType = ImageLinkType.resource; diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.ts b/ui-ngx/src/app/shared/components/image/image-dialog.component.ts index 40e7d2ec89..fd2b941382 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.ts @@ -146,6 +146,7 @@ export class ImageDialogComponent extends disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + imageSubType: this.image.resourceSubType, image: this.image } }).afterClosed().subscribe((result) => { diff --git a/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html index 8e24e2ffb2..3c4b6b62fd 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html @@ -24,6 +24,7 @@ , protected router: Router, - private imageService: ImageService, - private dialog: MatDialog, + @Inject(MAT_DIALOG_DATA) public data: ImageGalleryDialogData, public dialogRef: MatDialogRef) { super(store, router, dialogRef); } diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html index 21b10e8c66..d9768baf8a 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html @@ -22,7 +22,7 @@ [fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}">
- image.gallery + {{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}
@@ -46,7 +46,7 @@ {{ 'image.include-system-images' | translate }} + (ngModelChange)="includeSystemImagesChanged($event)">{{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }}
@@ -68,7 +68,7 @@ @@ -77,7 +77,7 @@ mat-icon-button color="primary" (click)="uploadImage()" - matTooltip="{{'image.upload-image' | translate }}" + matTooltip="{{ (isScada ? 'scada.upload-symbol' : 'image.upload-image' ) | translate }}" matTooltipPosition="above"> add @@ -87,18 +87,18 @@ mat-flat-button color="primary" (click)="uploadImage()"> - {{ 'image.upload-image' | translate }} + {{ (isScada ? 'scada.upload-symbol' : 'image.upload-image' ) | translate }}
{{ 'image.include-system-images' | translate }} + (ngModelChange)="includeSystemImagesChanged($event)">{{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }}
@@ -106,7 +106,7 @@   + placeholder="{{ (isScada ? 'scada.search' : 'image.search' ) | translate }}"/> - -
@@ -318,7 +318,7 @@
-
image.no-images
+
{{ (isScada ? 'scada.no-symbols' : 'image.no-images') | translate }}
@@ -326,18 +326,18 @@
- +
`); + const addTagButton = addTagPanel.find('.add-tag-button'); + addTagButton.on('click', () => { + this.setupAddTagInputPanel(); + }); + this.tooltip.content(addTagPanel); + this.tooltip.off('closing'); + } + + private setupAddTagInputPanel() { + const addTagInputPanel = + $(`
+ Enter tag: + + done + close +
`); + const tagInput = addTagInputPanel.find('input.tag-input'); + const applyTagButton = addTagInputPanel.find('span.apply-icon'); + const closeButton = addTagInputPanel.find('span.close-icon'); + textTooltip(applyTagButton, 'Apply'); + textTooltip(closeButton, 'Cancel'); + + tagInput.on('keypress', (event) => { + if (event.which === 13) { + const newTag: string = tagInput.val() as string; + if (newTag) { + this.setTag(newTag); + } + } + }); + applyTagButton.on('click', () => { + const newTag: string = tagInput.val() as string; + if (newTag) { + this.setTag(newTag); + } else { + this.tooltip.close(); + } + }); + closeButton.on('click', () => { + this.tooltip.close(); + }); + this.tooltip.content(addTagInputPanel); + this.tooltip.on('closing', () => { + this.setupAddTagPanel(); + }); + tagInput.trigger('focus'); + } + + private innerTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { + if (!this.isGroup()) { + const clientRect = helper.origin.getBoundingClientRect(); + position.coord.top = clientRect.top + (clientRect.height - position.size.height) / 2 + + this.innerTooltipOffset; + position.coord.left = clientRect.left + (clientRect.width - position.size.width) / 2; + } + return position; + } + + private hasTag() { + return !!this.tag; + } + + private isGroup() { + return this.element.type === 'g'; + } + } export class IotSvgObject { From b034cd1532175adf5b7283ee9a73ec58223305cd Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 16 May 2024 18:07:53 +0300 Subject: [PATCH 017/331] UI: Improve scada symbol editor --- ui-ngx/package.json | 1 + .../widget/lib/svg/iot-svg.models.ts | 379 +++++++++++++++--- .../scada-symbol/scada-symbol.component.html | 1 + ui-ngx/src/styles.scss | 143 +++++++ ui-ngx/yarn.lock | 9 +- 5 files changed, 482 insertions(+), 51 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b8a8e4c9a9..dc1c00f024 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -45,6 +45,7 @@ "@ngx-translate/core": "^14.0.0", "@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.js": "^3.2.0", + "@svgdotjs/svg.panzoom.js": "^2.1.2", "@tinymce/tinymce-angular": "^7.0.0", "ace-builds": "1.4.13", "ace-diff": "^3.0.3", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index fbab201b72..e128a26675 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -16,7 +16,8 @@ import { ValueType } from '@shared/models/constants'; import * as svgjs from '@svgdotjs/svg.js'; -import { Box, Element, Rect, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js'; +import { Box, Element, Rect, Runner, SVG, Svg, Text, Timeline, Style } from '@svgdotjs/svg.js'; +import '@svgdotjs/svg.panzoom.js'; import { DataToValueType, GetValueAction, @@ -45,6 +46,7 @@ import { ResizeObserver } from '@juggle/resize-observer'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipPosition = JQueryTooltipster.ITooltipPosition; import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper; +import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; export interface IotSvgApi { formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; @@ -281,30 +283,36 @@ export class IotSvgEditObject { private box: Box; private elements: IotSvgElement[] = []; private readonly shapeResize$: ResizeObserver; + private performSetup = false; + private hoverFilterStyle: Style; public scale = 1; constructor(private rootElement: HTMLElement) { this.shapeResize$ = new ResizeObserver(() => { this.resize(); }); - this.shapeResize$.observe(this.rootElement); } public setContent(svgContent: string) { + this.shapeResize$.unobserve(this.rootElement); if (this.svgShape) { + this.elements.length = 0; this.svgShape.remove(); } const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); - this.svgShape = SVG().svg(doc.documentElement.innerHTML); + this.svgShape = SVG().addTo(this.rootElement).svg(doc.documentElement.innerHTML); this.svgShape.node.style.overflow = 'visible'; this.svgShape.node.style['user-select'] = 'none'; this.box = this.svgShape.bbox(); this.svgShape.size(this.box.width, this.box.height); - this.svgShape.addTo(this.rootElement); - this.resize(); - //this.svgShape.cre - this.svgShape.style().rule('.hovered', {filter: 'drop-shadow(0px 0px 1px #FFC107)'}); - //this.svgShape.style().rule('.hovered', {filter: 'opacity(50%)'}); + this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); this.svgShape.style().rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); + this.updateHoverFilterStyle(); + this.performSetup = true; + this.shapeResize$.observe(this.rootElement); + } + + private doSetup() { + this.setupZoomPan(0); (window as any).SVG = svgjs; forkJoin([ from(import('tooltipster')), @@ -314,6 +322,49 @@ export class IotSvgEditObject { }); } + private setupZoomPan(margin: number) { + this.svgShape.on('zoom', (e) => { + const { + detail: { level, focus } + } = e as any; + this.svgShape.zoom(level, focus); + const box = this.restrictToMargins(this.svgShape.viewbox(), margin); + this.svgShape.viewbox(box); + setTimeout(() => { + this.updateTooltipPositions(); + }); + e.preventDefault(); + }); + this.svgShape.on('panning', (e) => { + const box = (e as any).detail.box; + this.svgShape.viewbox(this.restrictToMargins(box, margin)); + setTimeout(() => { + this.updateTooltipPositions(); + }); + e.preventDefault(); + }); + this.svgShape.on('panStart', (e) => { + this.svgShape.node.style.cursor = 'grab'; + }); + this.svgShape.on('panEnd', (e) => { + this.svgShape.node.style.cursor = 'default'; + }); + } + + private restrictToMargins(box: Box, margin: number): Box { + if (box.x < -margin) { + box.x = -margin; + } else if ((box.x + box.width) > (this.box.width + margin)) { + box.x = this.box.width + margin - box.width; + } + if (box.y < -margin) { + box.y = -margin; + } else if ((box.y + box.height) > (this.box.height + margin)) { + box.y = this.box.height + margin - box.height; + } + return box; + } + private setupElements() { this.svgShape.children().forEach(child => { this.addElement(child); @@ -343,9 +394,20 @@ export class IotSvgEditObject { } } for (const group of overlappingGroups) { - let offset = - (elementTooltipMinHeight * group.length) / 2 + elementTooltipMinHeight / 2; + const centers = group.map(e => e.box.cy); + const center = centers.reduce((a, b) => a + b, 0) / centers.length; + const textElement = group.find(e => e.isText()); + const slots = textElement ? group.length % 2 === 0 ? (group.length + 1) : group.length : group.length; + if (textElement) { + textElement.setInnerTooltipOffset(0, center); + group.splice(group.indexOf(textElement), 1); + } + let offset = - (elementTooltipMinHeight * slots) / 2 + elementTooltipMinHeight / 2; for (const element of group) { - element.innerTooltipOffset = offset; + if (textElement && offset === 0) { + offset += elementTooltipMinHeight; + } + element.setInnerTooltipOffset(offset, center); offset += elementTooltipMinHeight; } } @@ -376,12 +438,55 @@ export class IotSvgEditObject { if (this.svgShape) { const targetWidth = this.rootElement.getBoundingClientRect().width; const targetHeight = this.rootElement.getBoundingClientRect().height; + let scale: number; if (targetWidth < targetHeight) { - this.scale = targetWidth / this.box.width; + scale = targetWidth / this.box.width; } else { - this.scale = targetHeight / this.box.height; + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + this.updateHoverFilterStyle(); + this.updateZoomOptions(); + this.updateTooltipPositions(); + } + if (this.performSetup) { + this.performSetup = false; + this.doSetup(); } - this.svgShape.node.style.transform = `scale(${this.scale})`; + } + } + + private updateHoverFilterStyle() { + if (this.hoverFilterStyle) { + this.hoverFilterStyle.remove(); + } + const whiteBlur = (2.8 / this.scale).toFixed(2); + const blackBlur = (1.2 / this.scale).toFixed(2); + this.hoverFilterStyle = + this.svgShape.style().rule('.hovered', + { + filter: + `drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) + drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) + drop-shadow(0px 0px ${blackBlur}px black)` + } + ); + } + + private updateZoomOptions() { + this.svgShape.panZoom({ + zoomMin: 1, + zoomMax: 4, + zoomFactor: 2 / this.scale + }); + } + + private updateTooltipPositions() { + const container = this.rootElement.getBoundingClientRect(); + for (const e of this.elements) { + e.updateTooltipPosition(container); } } @@ -390,8 +495,8 @@ export class IotSvgEditObject { const hasBBox = (e: Element): boolean => { try { if (e.bbox) { - e.bbox(); - return true; + const box = e.bbox(); + return !!box.width || !!box.height; } else { return false; } @@ -402,31 +507,45 @@ const hasBBox = (e: Element): boolean => { const textTooltip = (el: JQuery, text: string) => { el.tooltipster({ - theme: ['tooltipster-tb'], + theme: ['iot-svg'], trigger: 'hover', content: text }); }; +const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => ( + target.left >= container.left - horizontalGap && + target.left + target.width <= container.left + container.width + horizontalGap && + target.top >= container.top - verticalGap && + target.top + target.height <= container.top + container.height + verticalGap +); + const elementTooltipMinHeight = 36 + 8; const elementTooltipMinWidth = 100; -const groupRectStroke = 10; +const groupRectStroke = 2; +const groupRectPadding = 2; class IotSvgElement { private highlightRect: Rect; + private highlightRectTimeline: Timeline; private tooltip: ITooltipsterInstance; private tag: string; - public innerTooltipOffset = 0; + private isEditing = false; + + private innerTooltipOffset = 0; public readonly box: Box; private highlighted = false; + private tooltipMouseX: number; + private tooltipMouseY: number; + constructor(private editObject: IotSvgEditObject, private element: Element) { this.tag = element.attr('tb:tag'); @@ -437,10 +556,18 @@ class IotSvgElement { if (this.isGroup()) { this.highlightRect = this.editObject.svgShape - .rect(this.box.width + this.unscaled(groupRectStroke * 4), this.box.height + this.unscaled(groupRectStroke * 4)) - .x(this.box.x - this.unscaled(groupRectStroke * 2)) - .y(this.box.y - this.unscaled(groupRectStroke * 2)) - .attr({fill: 'none', stroke: '#ccc', 'stroke-width': this.unscaled(groupRectStroke), opacity: 0}); + .rect(this.box.width + groupRectPadding * 2, this.box.height + groupRectPadding * 2) + .x(this.box.x - groupRectPadding) + .y(this.box.y - groupRectPadding) + .attr({ + fill: 'none', + rx: this.unscaled(6), + stroke: 'rgba(0, 0, 0, 0.38)', + 'stroke-dasharray': '1', + 'stroke-width': this.unscaled(groupRectStroke), + opacity: 0}); + this.highlightRectTimeline = new Timeline(); + this.highlightRect.timeline(this.highlightRectTimeline); this.highlightRect.hide(); } else { this.element.addClass('tb-element'); @@ -470,18 +597,22 @@ class IotSvgElement { if (!this.highlighted) { this.highlighted = true; if (this.isGroup()) { - this.highlightRect.width(this.box.width + this.unscaled(groupRectStroke * 4)) - .height(this.box.height + this.unscaled(groupRectStroke * 4)) - .x(this.box.x - this.unscaled(groupRectStroke * 2)) - .y(this.box.y - this.unscaled(groupRectStroke * 2)) - .attr({'stroke-width': this.unscaled(groupRectStroke)}); + this.highlightRectTimeline.finish(); + this.highlightRect + .attr({ + rx: this.unscaled(6), + 'stroke-width': this.unscaled(groupRectStroke) + }); this.highlightRect.show(); this.highlightRect.animate(300).attr({opacity: 1}); } else { this.element.addClass('hovered'); } if (this.hasTag()) { - this.tooltip.reposition(); + if (!this.isEditing) { + this.tooltip.reposition(); + } + $(this.tooltip.elementTooltip()).addClass('tb-active'); } } } @@ -490,12 +621,16 @@ class IotSvgElement { if (this.highlighted) { this.highlighted = false; if (this.isGroup()) { + this.highlightRectTimeline.finish(); this.highlightRect.animate(300).attr({opacity: 0}).after(() => { this.highlightRect.hide(); }); } else { this.element.removeClass('hovered'); } + if (this.hasTag()) { + $(this.tooltip.elementTooltip()).removeClass('tb-active'); + } } } @@ -503,6 +638,7 @@ class IotSvgElement { this.tooltip.destroy(); this.tag = null; this.element.attr('tb:tag', null); + this.unhighlight(); this.createAddTagTooltip(); } @@ -513,8 +649,31 @@ class IotSvgElement { this.createTagTooltip(); } + public updateTooltipPosition(container: DOMRect) { + if (this.tooltip && !this.tooltip.status().destroyed) { + this.tooltip.reposition(); + const tooltipElement = this.tooltip.elementTooltip(); + if (tooltipElement) { + if (isDomRectContained(tooltipElement.getBoundingClientRect(), container, + elementTooltipMinWidth, elementTooltipMinHeight)) { + tooltipElement.style.visibility = null; + } else { + tooltipElement.style.visibility = 'hidden'; + } + } + } + } + + public setInnerTooltipOffset(offset: number, center: number) { + this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale; + } + private unscaled(size: number): number { - return size / this.editObject.scale; + return size / (this.editObject.scale * this.editObject.svgShape.zoom()); + } + + private scaled(size: number): number { + return size * (this.editObject.scale * this.editObject.svgShape.zoom()); } private createTagTooltip() { @@ -522,17 +681,26 @@ class IotSvgElement { el.tooltipster( { arrow: this.isGroup(), - distance: this.isGroup() ? 20 : 6, - theme: ['tooltipster-tb'], + distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6, + theme: ['iot-svg'], delay: 0, animationDuration: 0, interactive: true, trigger: 'custom', side: 'top', - trackOrigin: true, + trackOrigin: false, content: '', functionPosition: (instance, helper, position) => - this.innerTooltipPosition(instance, helper, position) + this.innerTagTooltipPosition(instance, helper, position), + functionReady: (instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.highlight(); + }); + tooltipEl.on('mouseleave', () => { + this.unhighlight(); + }); + } } ); this.tooltip = el.tooltipster('instance'); @@ -540,6 +708,8 @@ class IotSvgElement { } private setupTagPanel() { + this.isEditing = false; + this.unhighlight(); const tagPanel = $(`
${this.element.type}: @@ -547,12 +717,6 @@ class IotSvgElement { edit delete
`); - tagPanel.on('mouseenter', () => { - this.highlight(); - }); - tagPanel.on('mouseleave', () => { - this.unhighlight(); - }); const updateTagButton = tagPanel.find('.edit-icon'); textTooltip(updateTagButton, 'Update tag'); updateTagButton.on('click', () => { @@ -568,6 +732,7 @@ class IotSvgElement { } private setupEditTagPanel() { + this.isEditing = true; const editTagInputPanel = $(`
Update tag: @@ -621,17 +786,38 @@ class IotSvgElement { const el = $(this.element.node); el.tooltipster( { - arrow: this.isGroup(), - distance: this.isGroup() ? 20 : 6, - theme: ['tooltipster-tb'], - delay: 200, + arrow: true, + theme: ['iot-svg', 'tb-active'], + delay: [0, 300], interactive: true, trigger: 'hover', - side: 'top', - trackOrigin: true, + side: ['top', 'left', 'bottom', 'right'], + trackOrigin: false, content: '', + functionBefore: (instance, helper) => { + const mouseEvent = (helper.event as MouseEvent); + this.tooltipMouseX = mouseEvent.clientX; + this.tooltipMouseY = mouseEvent.clientY; + let side: TooltipPositioningSide; + if (this.isGroup()) { + side = 'top'; + } else { + side = this.calculateTooltipSide(helper.origin.getBoundingClientRect(), + mouseEvent.clientX, mouseEvent.clientY); + } + instance.option('side', side); + }, functionPosition: (instance, helper, position) => - this.innerTooltipPosition(instance, helper, position) + this.innerAddTagTooltipPosition(instance, helper, position), + functionReady: (instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.highlight(); + }); + tooltipEl.on('mouseleave', () => { + this.unhighlight(); + }); + } } ); this.tooltip = el.tooltipster('instance'); @@ -639,6 +825,7 @@ class IotSvgElement { } private setupAddTagPanel() { + this.isEditing = false; const addTagPanel = $(`
${this.element.type}: @@ -653,6 +840,7 @@ class IotSvgElement { } private setupAddTagInputPanel() { + this.isEditing = true; const addTagInputPanel = $(`
Enter tag: @@ -666,50 +854,141 @@ class IotSvgElement { textTooltip(applyTagButton, 'Apply'); textTooltip(closeButton, 'Cancel'); + let addPanelClosed = false; tagInput.on('keypress', (event) => { if (event.which === 13) { const newTag: string = tagInput.val() as string; if (newTag) { + addPanelClosed = true; this.setTag(newTag); } } }); applyTagButton.on('click', () => { const newTag: string = tagInput.val() as string; + addPanelClosed = true; if (newTag) { this.setTag(newTag); } else { + this.unhighlight(); this.tooltip.close(); } }); closeButton.on('click', () => { + addPanelClosed = true; + this.unhighlight(); this.tooltip.close(); }); + tagInput.on('blur', () => { + setTimeout(() => { + if (!addPanelClosed) { + addPanelClosed = true; + this.tooltip.close(); + } + }); + }); this.tooltip.content(addTagInputPanel); + this.tooltip.option('delay', [0, 10000000]); this.tooltip.on('closing', () => { + this.tooltip.option('delay', [0, 300]); this.setupAddTagPanel(); }); tagInput.trigger('focus'); } - private innerTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { + private innerTagTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, + position: ITooltipPosition): ITooltipPosition { + const clientRect = helper.origin.getBoundingClientRect(); if (!this.isGroup()) { - const clientRect = helper.origin.getBoundingClientRect(); position.coord.top = clientRect.top + (clientRect.height - position.size.height) / 2 - + this.innerTooltipOffset; + + this.innerTooltipOffset * this.editObject.svgShape.zoom(); position.coord.left = clientRect.left + (clientRect.width - position.size.width) / 2; + } else { + position.distance = this.scaled(groupRectPadding) + groupRectStroke; + position.coord.top = clientRect.top - position.size.height - (this.scaled(groupRectPadding) + groupRectStroke); + } + return position; + } + + private innerAddTagTooltipPosition(instance: ITooltipsterInstance, + helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { + const distance = 10; + switch (position.side) { + case 'right': + position.coord.top = this.tooltipMouseY - position.size.height / 2; + position.coord.left = this.tooltipMouseX + distance; + position.target = this.tooltipMouseY; + break; + case 'top': + position.coord.top = this.tooltipMouseY - position.size.height - distance; + position.coord.left = this.tooltipMouseX - position.size.width / 2; + position.target = this.tooltipMouseX; + if (this.isGroup()) { + position.coord.top -= elementTooltipMinHeight; + } + break; + case 'left': + position.coord.top = this.tooltipMouseY - position.size.height / 2; + position.coord.left = this.tooltipMouseX - position.size.width - distance; + position.target = this.tooltipMouseY; + break; + case 'bottom': + position.coord.top = this.tooltipMouseY + distance; + position.coord.left = this.tooltipMouseX - position.size.width / 2; + position.target = this.tooltipMouseX; + break; } return position; } + private calculateTooltipSide(clientRect: DOMRect, mouseX: number, mouseY: number): TooltipPositioningSide { + let side: TooltipPositioningSide; + const cx = clientRect.left + clientRect.width / 2; + const cy = clientRect.top + clientRect.height / 2; + if (clientRect.width > clientRect.height) { + if (Math.abs(cx - mouseX) > clientRect.width / 4) { + if (mouseX < cx) { + side = 'left'; + } else { + side = 'right'; + } + } else { + if (mouseY < cy) { + side = 'top'; + } else { + side = 'bottom'; + } + } + } else { + if (Math.abs(cy - mouseY) > clientRect.height / 4) { + if (mouseY < cy) { + side = 'top'; + } else { + side = 'bottom'; + } + } else { + if (mouseX < cx) { + side = 'left'; + } else { + side = 'right'; + } + } + } + return side; + } + private hasTag() { return !!this.tag; } - private isGroup() { + public isGroup() { return this.element.type === 'g'; } + public isText() { + return this.element.type === 'text'; + } + } export class IotSvgObject { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index cd366abbde..0aa5a589f3 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -27,6 +27,7 @@
{{ metadata | json }} diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 4d5ae09a15..0c59e74ee5 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -460,6 +460,149 @@ mat-icon { } } +.tooltipster-sidetip.iot-svg { + .tooltipster-box { + user-select: none; + background: rgba(0, 0, 0, 0.54); + border: 1px solid rgba(0, 0, 0, 0); + border-radius: 8px; + .tooltipster-content { + padding: 4px 8px; + font-size: 12px; + line-height: 12px; + font-weight: 500; + color: #ffffff; + } + } + &.tb-active { + .tooltipster-box { + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.38); + box-shadow: 0 0 10px 6px rgba(0, 0, 0, .2); + .tooltipster-content { + color: #000; + } + } + .tooltipster-arrow { + .tooltipster-arrow-uncropped { + .tooltipster-arrow-background { + border-width: 12px; + } + } + } + &.tooltipster-top { + .tooltipster-arrow { + bottom: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-top-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-top-color: #fff; + left: -2px; + } + } + } + } + &.tooltipster-bottom { + .tooltipster-arrow { + top: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-bottom-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-bottom-color: #fff; + left: -2px; + top: -1px; + } + } + } + } + &.tooltipster-left { + .tooltipster-arrow { + right: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-left-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-left-color: #fff; + top: -2px; + } + } + } + } + &.tooltipster-right { + .tooltipster-arrow { + left: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-right-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-right-color: #fff; + top: -2px; + left: -1px; + } + } + } + } + } + &.tooltipster-top { + .tooltipster-arrow { + bottom: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-top-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-top-color: transparent; + } + } + } + } + &.tooltipster-bottom { + .tooltipster-arrow { + top: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-bottom-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-bottom-color: transparent; + } + } + } + } + &.tooltipster-left { + .tooltipster-arrow { + right: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-left-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-left-color: transparent; + } + } + } + } + &.tooltipster-right { + .tooltipster-arrow { + left: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-right-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-right-color: transparent; + } + } + } + } +} + .tb-default, .tb-dark { /********************************* diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 68e9ea2523..0f46efe018 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -2695,11 +2695,18 @@ dependencies: "@svgdotjs/svg.js" "^3.1.1" -"@svgdotjs/svg.js@^3.1.1", "@svgdotjs/svg.js@^3.2.0": +"@svgdotjs/svg.js@^3.0.16", "@svgdotjs/svg.js@^3.1.1", "@svgdotjs/svg.js@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz#6baa8cef6778a93818ac18faa2055222e60aa644" integrity sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA== +"@svgdotjs/svg.panzoom.js@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.panzoom.js/-/svg.panzoom.js-2.1.2.tgz#50e66a861f7c4f9e3992707f8e62e6e8da5c5223" + integrity sha512-0Nzo2TRlTebW3pzfAPtHx8Ye7Y3kuMEkK7hwVJi0SgQUB/vstjg7fvCJxB++EqsuDEetP0/SC+4CpLMVm6Lh2g== + dependencies: + "@svgdotjs/svg.js" "^3.0.16" + "@tinymce/tinymce-angular@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-7.0.0.tgz#010de497d5774a8bdc5d5936bf4fb976adf05f56" From 97adcde9d18667e9f9ec3eb40a47bd8c2f85253b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 17 May 2024 19:31:35 +0300 Subject: [PATCH 018/331] UI: Edit SCADA symbol UI refactoring. --- .../widget/lib/svg/iot-svg.models.ts | 724 +--------------- .../scada-symbol-editor.component.html | 18 + .../scada-symbol-editor.component.scss | 22 + .../scada-symbol-editor.component.ts | 83 ++ .../scada-symbol-tooltip.component.scss | 52 ++ .../scada-symbol-tooltip.components.ts | 295 +++++++ .../scada-symbol/scada-symbol.component.html | 2 +- .../scada-symbol/scada-symbol.component.scss | 7 - .../scada-symbol/scada-symbol.component.ts | 29 +- .../pages/scada-symbol/scada-symbol.models.ts | 799 ++++++++++++++++++ .../pages/scada-symbol/scada-symbol.module.ts | 6 +- ui-ngx/src/styles.scss | 9 +- 12 files changed, 1287 insertions(+), 759 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index e128a26675..db649d8ad1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -15,8 +15,7 @@ /// import { ValueType } from '@shared/models/constants'; -import * as svgjs from '@svgdotjs/svg.js'; -import { Box, Element, Rect, Runner, SVG, Svg, Text, Timeline, Style } from '@svgdotjs/svg.js'; +import { Box, Element, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js'; import '@svgdotjs/svg.panzoom.js'; import { DataToValueType, @@ -34,7 +33,7 @@ import { mergeDeep, parseFunction } from '@core/utils'; -import { BehaviorSubject, forkJoin, from, Observable, Observer } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs'; import { map, share } from 'rxjs/operators'; import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -42,11 +41,6 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetAction, WidgetActionType } from '@shared/models/widget.models'; -import { ResizeObserver } from '@juggle/resize-observer'; -import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; -import ITooltipPosition = JQueryTooltipster.ITooltipPosition; -import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper; -import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; export interface IotSvgApi { formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; @@ -277,720 +271,6 @@ export type IotSvgObjectSettings = {[id: string]: any}; const parseError = (ctx: WidgetContext, err: any): string => ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; -export class IotSvgEditObject { - - public svgShape: Svg; - private box: Box; - private elements: IotSvgElement[] = []; - private readonly shapeResize$: ResizeObserver; - private performSetup = false; - private hoverFilterStyle: Style; - public scale = 1; - constructor(private rootElement: HTMLElement) { - this.shapeResize$ = new ResizeObserver(() => { - this.resize(); - }); - } - - public setContent(svgContent: string) { - this.shapeResize$.unobserve(this.rootElement); - if (this.svgShape) { - this.elements.length = 0; - this.svgShape.remove(); - } - const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); - this.svgShape = SVG().addTo(this.rootElement).svg(doc.documentElement.innerHTML); - this.svgShape.node.style.overflow = 'visible'; - this.svgShape.node.style['user-select'] = 'none'; - this.box = this.svgShape.bbox(); - this.svgShape.size(this.box.width, this.box.height); - this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); - this.svgShape.style().rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); - this.updateHoverFilterStyle(); - this.performSetup = true; - this.shapeResize$.observe(this.rootElement); - } - - private doSetup() { - this.setupZoomPan(0); - (window as any).SVG = svgjs; - forkJoin([ - from(import('tooltipster')), - from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js')) - ]).subscribe(() => { - this.setupElements(); - }); - } - - private setupZoomPan(margin: number) { - this.svgShape.on('zoom', (e) => { - const { - detail: { level, focus } - } = e as any; - this.svgShape.zoom(level, focus); - const box = this.restrictToMargins(this.svgShape.viewbox(), margin); - this.svgShape.viewbox(box); - setTimeout(() => { - this.updateTooltipPositions(); - }); - e.preventDefault(); - }); - this.svgShape.on('panning', (e) => { - const box = (e as any).detail.box; - this.svgShape.viewbox(this.restrictToMargins(box, margin)); - setTimeout(() => { - this.updateTooltipPositions(); - }); - e.preventDefault(); - }); - this.svgShape.on('panStart', (e) => { - this.svgShape.node.style.cursor = 'grab'; - }); - this.svgShape.on('panEnd', (e) => { - this.svgShape.node.style.cursor = 'default'; - }); - } - - private restrictToMargins(box: Box, margin: number): Box { - if (box.x < -margin) { - box.x = -margin; - } else if ((box.x + box.width) > (this.box.width + margin)) { - box.x = this.box.width + margin - box.width; - } - if (box.y < -margin) { - box.y = -margin; - } else if ((box.y + box.height) > (this.box.height + margin)) { - box.y = this.box.height + margin - box.height; - } - return box; - } - - private setupElements() { - this.svgShape.children().forEach(child => { - this.addElement(child); - }); - const overlappingGroups: IotSvgElement[][] = []; - for (const el of this.elements) { - for (const other of this.elements) { - if (el !== other && el.overlappingCenters(other)) { - let overlappingGroup: IotSvgElement[]; - for (const list of overlappingGroups) { - if (list.includes(other) || list.includes(el)) { - overlappingGroup = list; - break; - } - } - if (!overlappingGroup) { - overlappingGroup = [el, other]; - overlappingGroups.push(overlappingGroup); - } else { - if (!overlappingGroup.includes(el)) { - overlappingGroup.push(el); - } else if (!overlappingGroup.includes(other)){ - overlappingGroup.push(other); - } - } - } - } - } - for (const group of overlappingGroups) { - const centers = group.map(e => e.box.cy); - const center = centers.reduce((a, b) => a + b, 0) / centers.length; - const textElement = group.find(e => e.isText()); - const slots = textElement ? group.length % 2 === 0 ? (group.length + 1) : group.length : group.length; - if (textElement) { - textElement.setInnerTooltipOffset(0, center); - group.splice(group.indexOf(textElement), 1); - } - let offset = - (elementTooltipMinHeight * slots) / 2 + elementTooltipMinHeight / 2; - for (const element of group) { - if (textElement && offset === 0) { - offset += elementTooltipMinHeight; - } - element.setInnerTooltipOffset(offset, center); - offset += elementTooltipMinHeight; - } - } - for (const el of this.elements) { - el.init(); - } - } - - private addElement(e: Element) { - if (hasBBox(e)) { - const iotSvgElement = new IotSvgElement(this, e); - this.elements.push(iotSvgElement); - e.children().forEach(child => { - if (!(child.type === 'tspan' && e.type === 'text')) { - this.addElement(child); - } - }, true); - } - } - - public destroy() { - if (this.shapeResize$) { - this.shapeResize$.disconnect(); - } - } - - private resize() { - if (this.svgShape) { - const targetWidth = this.rootElement.getBoundingClientRect().width; - const targetHeight = this.rootElement.getBoundingClientRect().height; - let scale: number; - if (targetWidth < targetHeight) { - scale = targetWidth / this.box.width; - } else { - scale = targetHeight / this.box.height; - } - if (this.scale !== scale) { - this.scale = scale; - this.svgShape.node.style.transform = `scale(${this.scale})`; - this.updateHoverFilterStyle(); - this.updateZoomOptions(); - this.updateTooltipPositions(); - } - if (this.performSetup) { - this.performSetup = false; - this.doSetup(); - } - } - } - - private updateHoverFilterStyle() { - if (this.hoverFilterStyle) { - this.hoverFilterStyle.remove(); - } - const whiteBlur = (2.8 / this.scale).toFixed(2); - const blackBlur = (1.2 / this.scale).toFixed(2); - this.hoverFilterStyle = - this.svgShape.style().rule('.hovered', - { - filter: - `drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) - drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) - drop-shadow(0px 0px ${blackBlur}px black)` - } - ); - } - - private updateZoomOptions() { - this.svgShape.panZoom({ - zoomMin: 1, - zoomMax: 4, - zoomFactor: 2 / this.scale - }); - } - - private updateTooltipPositions() { - const container = this.rootElement.getBoundingClientRect(); - for (const e of this.elements) { - e.updateTooltipPosition(container); - } - } - -} - -const hasBBox = (e: Element): boolean => { - try { - if (e.bbox) { - const box = e.bbox(); - return !!box.width || !!box.height; - } else { - return false; - } - } catch (_e) { - return false; - } -}; - -const textTooltip = (el: JQuery, text: string) => { - el.tooltipster({ - theme: ['iot-svg'], - trigger: 'hover', - content: text - }); -}; - -const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => ( - target.left >= container.left - horizontalGap && - target.left + target.width <= container.left + container.width + horizontalGap && - target.top >= container.top - verticalGap && - target.top + target.height <= container.top + container.height + verticalGap -); - -const elementTooltipMinHeight = 36 + 8; -const elementTooltipMinWidth = 100; - -const groupRectStroke = 2; -const groupRectPadding = 2; - -class IotSvgElement { - - private highlightRect: Rect; - private highlightRectTimeline: Timeline; - - private tooltip: ITooltipsterInstance; - - private tag: string; - - private isEditing = false; - - private innerTooltipOffset = 0; - - public readonly box: Box; - - private highlighted = false; - - private tooltipMouseX: number; - private tooltipMouseY: number; - - constructor(private editObject: IotSvgEditObject, - private element: Element) { - this.tag = element.attr('tb:tag'); - this.box = element.rbox(this.editObject.svgShape); - } - - public init() { - if (this.isGroup()) { - this.highlightRect = - this.editObject.svgShape - .rect(this.box.width + groupRectPadding * 2, this.box.height + groupRectPadding * 2) - .x(this.box.x - groupRectPadding) - .y(this.box.y - groupRectPadding) - .attr({ - fill: 'none', - rx: this.unscaled(6), - stroke: 'rgba(0, 0, 0, 0.38)', - 'stroke-dasharray': '1', - 'stroke-width': this.unscaled(groupRectStroke), - opacity: 0}); - this.highlightRectTimeline = new Timeline(); - this.highlightRect.timeline(this.highlightRectTimeline); - this.highlightRect.hide(); - } else { - this.element.addClass('tb-element'); - } - this.element.on('mouseenter', (event) => { - this.highlight(); - }); - this.element.on('mouseleave', (event) => { - this.unhighlight(); - }); - if (this.hasTag()) { - this.createTagTooltip(); - } else { - this.createAddTagTooltip(); - } - } - - public overlappingCenters(otherElement: IotSvgElement): boolean { - if (this.isGroup() || otherElement.isGroup()) { - return false; - } - return Math.abs(this.box.cx - otherElement.box.cx) * this.editObject.scale < elementTooltipMinWidth && - Math.abs(this.box.cy - otherElement.box.cy) * this.editObject.scale < elementTooltipMinHeight; - } - - public highlight() { - if (!this.highlighted) { - this.highlighted = true; - if (this.isGroup()) { - this.highlightRectTimeline.finish(); - this.highlightRect - .attr({ - rx: this.unscaled(6), - 'stroke-width': this.unscaled(groupRectStroke) - }); - this.highlightRect.show(); - this.highlightRect.animate(300).attr({opacity: 1}); - } else { - this.element.addClass('hovered'); - } - if (this.hasTag()) { - if (!this.isEditing) { - this.tooltip.reposition(); - } - $(this.tooltip.elementTooltip()).addClass('tb-active'); - } - } - } - - public unhighlight() { - if (this.highlighted) { - this.highlighted = false; - if (this.isGroup()) { - this.highlightRectTimeline.finish(); - this.highlightRect.animate(300).attr({opacity: 0}).after(() => { - this.highlightRect.hide(); - }); - } else { - this.element.removeClass('hovered'); - } - if (this.hasTag()) { - $(this.tooltip.elementTooltip()).removeClass('tb-active'); - } - } - } - - public clearTag() { - this.tooltip.destroy(); - this.tag = null; - this.element.attr('tb:tag', null); - this.unhighlight(); - this.createAddTagTooltip(); - } - - public setTag(tag: string) { - this.tooltip.destroy(); - this.tag = tag; - this.element.attr('tb:tag', tag); - this.createTagTooltip(); - } - - public updateTooltipPosition(container: DOMRect) { - if (this.tooltip && !this.tooltip.status().destroyed) { - this.tooltip.reposition(); - const tooltipElement = this.tooltip.elementTooltip(); - if (tooltipElement) { - if (isDomRectContained(tooltipElement.getBoundingClientRect(), container, - elementTooltipMinWidth, elementTooltipMinHeight)) { - tooltipElement.style.visibility = null; - } else { - tooltipElement.style.visibility = 'hidden'; - } - } - } - } - - public setInnerTooltipOffset(offset: number, center: number) { - this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale; - } - - private unscaled(size: number): number { - return size / (this.editObject.scale * this.editObject.svgShape.zoom()); - } - - private scaled(size: number): number { - return size * (this.editObject.scale * this.editObject.svgShape.zoom()); - } - - private createTagTooltip() { - const el = $(this.element.node); - el.tooltipster( - { - arrow: this.isGroup(), - distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6, - theme: ['iot-svg'], - delay: 0, - animationDuration: 0, - interactive: true, - trigger: 'custom', - side: 'top', - trackOrigin: false, - content: '', - functionPosition: (instance, helper, position) => - this.innerTagTooltipPosition(instance, helper, position), - functionReady: (instance, helper) => { - const tooltipEl = $(helper.tooltip); - tooltipEl.on('mouseenter', () => { - this.highlight(); - }); - tooltipEl.on('mouseleave', () => { - this.unhighlight(); - }); - } - } - ); - this.tooltip = el.tooltipster('instance'); - this.setupTagPanel(); - } - - private setupTagPanel() { - this.isEditing = false; - this.unhighlight(); - const tagPanel = - $(`
- ${this.element.type}: - ${this.tag} - edit - delete -
`); - const updateTagButton = tagPanel.find('.edit-icon'); - textTooltip(updateTagButton, 'Update tag'); - updateTagButton.on('click', () => { - this.setupEditTagPanel(); - }); - const deleteButton = tagPanel.find('.delete-icon'); - textTooltip(deleteButton, 'Remove tag'); - deleteButton.on('click', () => { - this.clearTag(); - }); - this.tooltip.content(tagPanel); - this.tooltip.open(); - } - - private setupEditTagPanel() { - this.isEditing = true; - const editTagInputPanel = - $(`
- Update tag: - - done - close -
`); - const tagInput = editTagInputPanel.find('input.tag-input'); - const applyTagButton = editTagInputPanel.find('span.apply-icon'); - const closeButton = editTagInputPanel.find('span.close-icon'); - textTooltip(applyTagButton, 'Apply'); - textTooltip(closeButton, 'Cancel'); - tagInput.val(this.tag); - let editPanelClosed = false; - - tagInput.on('keypress', (event) => { - if (event.which === 13) { - const newTag: string = tagInput.val() as string; - if (newTag) { - editPanelClosed = true; - this.setTag(newTag); - } - } - }); - applyTagButton.on('click', () => { - const newTag: string = tagInput.val() as string; - editPanelClosed = true; - if (newTag) { - this.setTag(newTag); - } else { - this.setupTagPanel(); - } - }); - closeButton.on('click', () => { - editPanelClosed = true; - this.setupTagPanel(); - }); - tagInput.on('blur', () => { - setTimeout(() => { - if (!editPanelClosed) { - editPanelClosed = true; - this.setupTagPanel(); - } - }); - }); - this.tooltip.content(editTagInputPanel); - tagInput.trigger('focus'); - } - - private createAddTagTooltip() { - const el = $(this.element.node); - el.tooltipster( - { - arrow: true, - theme: ['iot-svg', 'tb-active'], - delay: [0, 300], - interactive: true, - trigger: 'hover', - side: ['top', 'left', 'bottom', 'right'], - trackOrigin: false, - content: '', - functionBefore: (instance, helper) => { - const mouseEvent = (helper.event as MouseEvent); - this.tooltipMouseX = mouseEvent.clientX; - this.tooltipMouseY = mouseEvent.clientY; - let side: TooltipPositioningSide; - if (this.isGroup()) { - side = 'top'; - } else { - side = this.calculateTooltipSide(helper.origin.getBoundingClientRect(), - mouseEvent.clientX, mouseEvent.clientY); - } - instance.option('side', side); - }, - functionPosition: (instance, helper, position) => - this.innerAddTagTooltipPosition(instance, helper, position), - functionReady: (instance, helper) => { - const tooltipEl = $(helper.tooltip); - tooltipEl.on('mouseenter', () => { - this.highlight(); - }); - tooltipEl.on('mouseleave', () => { - this.unhighlight(); - }); - } - } - ); - this.tooltip = el.tooltipster('instance'); - this.setupAddTagPanel(); - } - - private setupAddTagPanel() { - this.isEditing = false; - const addTagPanel = - $(`
- ${this.element.type}: - -
`); - const addTagButton = addTagPanel.find('.add-tag-button'); - addTagButton.on('click', () => { - this.setupAddTagInputPanel(); - }); - this.tooltip.content(addTagPanel); - this.tooltip.off('closing'); - } - - private setupAddTagInputPanel() { - this.isEditing = true; - const addTagInputPanel = - $(`
- Enter tag: - - done - close -
`); - const tagInput = addTagInputPanel.find('input.tag-input'); - const applyTagButton = addTagInputPanel.find('span.apply-icon'); - const closeButton = addTagInputPanel.find('span.close-icon'); - textTooltip(applyTagButton, 'Apply'); - textTooltip(closeButton, 'Cancel'); - - let addPanelClosed = false; - tagInput.on('keypress', (event) => { - if (event.which === 13) { - const newTag: string = tagInput.val() as string; - if (newTag) { - addPanelClosed = true; - this.setTag(newTag); - } - } - }); - applyTagButton.on('click', () => { - const newTag: string = tagInput.val() as string; - addPanelClosed = true; - if (newTag) { - this.setTag(newTag); - } else { - this.unhighlight(); - this.tooltip.close(); - } - }); - closeButton.on('click', () => { - addPanelClosed = true; - this.unhighlight(); - this.tooltip.close(); - }); - tagInput.on('blur', () => { - setTimeout(() => { - if (!addPanelClosed) { - addPanelClosed = true; - this.tooltip.close(); - } - }); - }); - this.tooltip.content(addTagInputPanel); - this.tooltip.option('delay', [0, 10000000]); - this.tooltip.on('closing', () => { - this.tooltip.option('delay', [0, 300]); - this.setupAddTagPanel(); - }); - tagInput.trigger('focus'); - } - - private innerTagTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, - position: ITooltipPosition): ITooltipPosition { - const clientRect = helper.origin.getBoundingClientRect(); - if (!this.isGroup()) { - position.coord.top = clientRect.top + (clientRect.height - position.size.height) / 2 - + this.innerTooltipOffset * this.editObject.svgShape.zoom(); - position.coord.left = clientRect.left + (clientRect.width - position.size.width) / 2; - } else { - position.distance = this.scaled(groupRectPadding) + groupRectStroke; - position.coord.top = clientRect.top - position.size.height - (this.scaled(groupRectPadding) + groupRectStroke); - } - return position; - } - - private innerAddTagTooltipPosition(instance: ITooltipsterInstance, - helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { - const distance = 10; - switch (position.side) { - case 'right': - position.coord.top = this.tooltipMouseY - position.size.height / 2; - position.coord.left = this.tooltipMouseX + distance; - position.target = this.tooltipMouseY; - break; - case 'top': - position.coord.top = this.tooltipMouseY - position.size.height - distance; - position.coord.left = this.tooltipMouseX - position.size.width / 2; - position.target = this.tooltipMouseX; - if (this.isGroup()) { - position.coord.top -= elementTooltipMinHeight; - } - break; - case 'left': - position.coord.top = this.tooltipMouseY - position.size.height / 2; - position.coord.left = this.tooltipMouseX - position.size.width - distance; - position.target = this.tooltipMouseY; - break; - case 'bottom': - position.coord.top = this.tooltipMouseY + distance; - position.coord.left = this.tooltipMouseX - position.size.width / 2; - position.target = this.tooltipMouseX; - break; - } - return position; - } - - private calculateTooltipSide(clientRect: DOMRect, mouseX: number, mouseY: number): TooltipPositioningSide { - let side: TooltipPositioningSide; - const cx = clientRect.left + clientRect.width / 2; - const cy = clientRect.top + clientRect.height / 2; - if (clientRect.width > clientRect.height) { - if (Math.abs(cx - mouseX) > clientRect.width / 4) { - if (mouseX < cx) { - side = 'left'; - } else { - side = 'right'; - } - } else { - if (mouseY < cy) { - side = 'top'; - } else { - side = 'bottom'; - } - } - } else { - if (Math.abs(cy - mouseY) > clientRect.height / 4) { - if (mouseY < cy) { - side = 'top'; - } else { - side = 'bottom'; - } - } else { - if (mouseX < cx) { - side = 'left'; - } else { - side = 'right'; - } - } - } - return side; - } - - private hasTag() { - return !!this.tag; - } - - public isGroup() { - return this.element.type === 'g'; - } - - public isText() { - return this.element.type === 'text'; - } - -} - export class IotSvgObject { private metadata: IotSvgMetadata; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html new file mode 100644 index 0000000000..96e1018eec --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html @@ -0,0 +1,18 @@ + +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss new file mode 100644 index 0000000000..7bad327b9a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts new file mode 100644 index 0000000000..6f73ae7ac4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -0,0 +1,83 @@ +/// +/// Copyright ยฉ 2016-2024 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 { + AfterViewInit, + Component, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { ScadaSymbolEditObject } from '@home/pages/scada-symbol/scada-symbol.models'; + +@Component({ + selector: 'tb-scada-symbol-editor', + templateUrl: './scada-symbol-editor.component.html', + styleUrls: ['./scada-symbol-editor.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges { + + @ViewChild('iotSvgShape', {static: false}) + iotSvgShape: ElementRef; + + @Input() + svgContent: string; + + scadaSymbolEditObject: ScadaSymbolEditObject; + + private destroy$ = new Subject(); + + constructor(private viewContainerRef: ViewContainerRef) { + } + + ngOnInit(): void { + } + + ngAfterViewInit() { + this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.iotSvgShape.nativeElement, + this.viewContainerRef); + if (this.svgContent) { + this.scadaSymbolEditObject.setContent(this.svgContent); + } + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'svgContent') { + if (this.scadaSymbolEditObject) { + this.scadaSymbolEditObject.setContent(this.svgContent); + } + } + } + } + } + + ngOnDestroy() { + this.scadaSymbolEditObject.destroy(); + } + +} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss new file mode 100644 index 0000000000..c7fc243dc0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol-tooltip-panel { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .mdc-button { + line-height: 20px; + font-size: 12px; + &.mat-mdc-button-base { + height: 20px; + .mat-mdc-button-touch-target { + height: 20px; + } + } + } + + .mat-mdc-form-field.tb-inline-field { + font-size: 12px; + line-height: 12px; + .mat-mdc-text-field-wrapper { + padding: 0 8px; + .mat-mdc-form-field-flex { + .mat-mdc-form-field-infix { + padding-top: 4px; + padding-bottom: 4px; + min-height: 20px; + .mat-mdc-input-element { + font-size: 12px; + line-height: 12px; + height: 12px; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts new file mode 100644 index 0000000000..cf38552f7a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -0,0 +1,295 @@ +/// +/// Copyright ยฉ 2016-2024 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 { + AfterViewInit, + Component, + ComponentRef, + Directive, + ElementRef, + EventEmitter, + Input, + NgModule, + Output, + Type, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { ScadaSymbolElement } from '@home/pages/scada-symbol/scada-symbol.models'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { ENTER } from '@angular/cdk/keycodes'; + +@Directive() +abstract class ScadaSymbolPanelComponent implements AfterViewInit { + + @Input() + symbolElement: ScadaSymbolElement; + + @Output() + viewInited = new EventEmitter(); + + protected constructor(public element: ElementRef) { + } + + ngAfterViewInit() { + this.viewInited.emit(); + } +} + +@Component({ + template: `
+ {{ symbolElement?.element?.type }}: + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolAddTagPanelComponent extends ScadaSymbolPanelComponent { + + @Output() + addTag = new EventEmitter(); + + constructor(public element: ElementRef) { + super(element); + } + + public onAddTag() { + this.addTag.emit(); + } + +} + +@Component({ + template: `
+ {{ isAdd ? 'Enter tag:' : 'Update tag:' }} + + + + + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit { + + @ViewChild('tagField') + tagField: ElementRef; + + @Input() + isAdd: boolean; + + @Input() + tag: string; + + @Output() + apply = new EventEmitter(); + + @Output() + cancel = new EventEmitter(); + + private closed = false; + + constructor(public element: ElementRef) { + super(element); + } + + ngAfterViewInit() { + super.ngAfterViewInit(); + setTimeout(() => { + this.tagField.nativeElement.focus(); + }); + } + + public tagEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (this.tag) { + this.closed = true; + this.apply.emit(this.tag); + } + } + } + + public onApply() { + this.closed = true; + if (this.tag) { + this.apply.emit(this.tag); + } else { + this.cancel.emit(); + } + } + + public onCancel() { + this.closed = true; + this.cancel.emit(); + } + + public onBlur() { + setTimeout(() => { + if (!this.closed) { + this.closed = true; + this.cancel.emit(); + } + }, 300); + } + +} + +@Component({ + template: `
+ {{ symbolElement?.element?.type }}: + {{ symbolElement?.tag }} + + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit { + + @Output() + updateTag = new EventEmitter(); + + @Output() + removeTag = new EventEmitter(); + + constructor(public element: ElementRef) { + super(element); + } + + public onUpdateTag() { + this.updateTag.emit(); + } + + public onRemoveTag() { + this.removeTag.emit(); + } +} + +@NgModule({ + declarations: + [ + ScadaSymbolAddTagPanelComponent, + ScadaSymbolTagInputPanelComponent, + ScadaSymbolTagPanelComponent + ], + imports: [ + CommonModule, + SharedModule + ] +}) +export class ScadaSymbolTooltipComponentsModule { } + +export const setupAddTagPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef) => { + symbolElement.setEditing(false); + symbolElement.tooltip.off('close'); + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolAddTagPanelComponent); + componentRef.instance.addTag.subscribe(() => { + componentRef.destroy(); + setupTagInputPanelTooltip(symbolElement, container, true); + }); +}; + +export const setupTagPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef) => { + symbolElement.setEditing(false); + symbolElement.unhighlight(); + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolTagPanelComponent); + componentRef.instance.updateTag.subscribe(() => { + componentRef.destroy(); + setupTagInputPanelTooltip(symbolElement, container, false); + }); + componentRef.instance.removeTag.subscribe(() => { + componentRef.destroy(); + symbolElement.clearTag(); + }); + symbolElement.tooltip.open(); +}; + +const setupTagInputPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef, isAdd: boolean) => { + symbolElement.setEditing(true); + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolTagInputPanelComponent); + + componentRef.instance.isAdd = isAdd; + if (!isAdd) { + componentRef.instance.tag = symbolElement.tag; + } + componentRef.instance.apply.subscribe((newTag) => { + componentRef.destroy(); + if (isAdd) { + symbolElement.tooltip.off('close'); + } + symbolElement.setTag(newTag); + }); + componentRef.instance.cancel.subscribe(() => { + if (isAdd) { + symbolElement.unhighlight(); + symbolElement.tooltip.close(); + } else { + componentRef.destroy(); + setupTagPanelTooltip(symbolElement, container); + } + }); + if (isAdd) { + symbolElement.tooltip.option('delay', [0, 10000000]); + symbolElement.tooltip.on('close', () => { + componentRef.destroy(); + symbolElement.tooltip.option('delay', [0, 300]); + setupAddTagPanelTooltip(symbolElement, container); + }); + } +}; + +const setTooltipComponent = (symbolElement: ScadaSymbolElement, + container: ViewContainerRef, componentType: Type): ComponentRef => { + const componentRef = container.createComponent(componentType); + componentRef.instance.symbolElement = symbolElement; + componentRef.instance.viewInited.subscribe(() => { + if (symbolElement.tooltip.status().open) { + symbolElement.repositionTooltip(); + } + }); + const parentElement = componentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + symbolElement.tooltip.content(content); + return componentRef; +}; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index 0aa5a589f3..dc4e620f94 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -35,7 +35,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss index 783e2d842b..1f3208cc91 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss @@ -24,12 +24,5 @@ min-width: 0; min-height: 0; background: #fff; - .tb-scada-symbol-shape { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index d75ac137e6..0d521d0a4f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -14,16 +14,7 @@ /// limitations under the License. /// -import { - AfterViewInit, - Component, - ElementRef, - HostBinding, - OnDestroy, - OnInit, - ViewChild, - ViewEncapsulation -} from '@angular/core'; +import { AfterViewInit, Component, HostBinding, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -31,11 +22,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol.models'; -import { - IotSvgEditObject, - IotSvgMetadata, - parseIotSvgMetadataFromContent -} from '@home/components/widget/lib/svg/iot-svg.models'; +import { IotSvgMetadata, parseIotSvgMetadataFromContent } from '@home/components/widget/lib/svg/iot-svg.models'; @Component({ selector: 'tb-scada-symbol', @@ -48,12 +35,7 @@ export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDes @HostBinding('style.width') width = '100%'; @HostBinding('style.height') height = '100%'; - @ViewChild('iotSvgShape', {static: false}) - iotSvgShape: ElementRef; - symbolData: ScadaSymbolData; - - iotSvgObjectEdit: IotSvgEditObject; metadata: IotSvgMetadata; private destroy$ = new Subject(); @@ -76,10 +58,6 @@ export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDes } ngAfterViewInit() { - this.iotSvgObjectEdit = new IotSvgEditObject(this.iotSvgShape.nativeElement); - if (this.symbolData) { - this.iotSvgObjectEdit.setContent(this.symbolData.svgContent); - } } ngOnDestroy() { @@ -94,9 +72,6 @@ export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDes private init() { this.symbolData = this.route.snapshot.data.symbolData; this.metadata = parseIotSvgMetadataFromContent(this.symbolData.svgContent); - if (this.iotSvgObjectEdit) { - this.iotSvgObjectEdit.setContent(this.symbolData.svgContent); - } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts index 32b05f2265..d8ee8c1a64 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts @@ -15,8 +15,807 @@ /// import { ImageResourceInfo } from '@shared/models/resource.models'; +import * as svgjs from '@svgdotjs/svg.js'; +import { Box, Element, Rect, Style, SVG, Svg, Timeline } from '@svgdotjs/svg.js'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; +import { Directive, ElementRef, OnDestroy, Type, ViewContainerRef } from '@angular/core'; +import { forkJoin, from } from 'rxjs'; +import { TbInject } from '@shared/decorators/tb-inject'; +import { + setupAddTagPanelTooltip, + setupTagPanelTooltip +} from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; +import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper; +import ITooltipPosition = JQueryTooltipster.ITooltipPosition; export interface ScadaSymbolData { imageResource: ImageResourceInfo; svgContent: string; } + +export class ScadaSymbolEditObject { + + public svgShape: Svg; + private box: Box; + private elements: ScadaSymbolElement[] = []; + private readonly shapeResize$: ResizeObserver; + private performSetup = false; + private hoverFilterStyle: Style; + public scale = 1; + constructor(private rootElement: HTMLElement, + public viewContainerRef: ViewContainerRef) { + this.shapeResize$ = new ResizeObserver(() => { + this.resize(); + }); + } + + public setContent(svgContent: string) { + this.shapeResize$.unobserve(this.rootElement); + if (this.svgShape) { + this.elements.length = 0; + this.svgShape.remove(); + } + const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + this.svgShape = SVG().addTo(this.rootElement).svg(doc.documentElement.innerHTML); + this.svgShape.node.style.overflow = 'visible'; + this.svgShape.node.style['user-select'] = 'none'; + this.box = this.svgShape.bbox(); + this.svgShape.size(this.box.width, this.box.height); + this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); + this.svgShape.style().rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); + this.updateHoverFilterStyle(); + this.performSetup = true; + this.shapeResize$.observe(this.rootElement); + } + + public editingElement() { + return this.elements.find(e => e.isEditing()); + } + + private doSetup() { + this.setupZoomPan(0); + (window as any).SVG = svgjs; + forkJoin([ + from(import('tooltipster')), + from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js')) + ]).subscribe(() => { + this.setupElements(); + }); + } + + private setupZoomPan(margin: number) { + this.svgShape.on('zoom', (e) => { + const { + detail: { level, focus } + } = e as any; + this.svgShape.zoom(level, focus); + const box = this.restrictToMargins(this.svgShape.viewbox(), margin); + this.svgShape.viewbox(box); + setTimeout(() => { + this.updateTooltipPositions(); + }); + e.preventDefault(); + }); + this.svgShape.on('panning', (e) => { + const box = (e as any).detail.box; + this.svgShape.viewbox(this.restrictToMargins(box, margin)); + setTimeout(() => { + this.updateTooltipPositions(); + }); + e.preventDefault(); + }); + this.svgShape.on('panStart', (e) => { + this.svgShape.node.style.cursor = 'grab'; + }); + this.svgShape.on('panEnd', (e) => { + this.svgShape.node.style.cursor = 'default'; + }); + } + + private restrictToMargins(box: Box, margin: number): Box { + if (box.x < -margin) { + box.x = -margin; + } else if ((box.x + box.width) > (this.box.width + margin)) { + box.x = this.box.width + margin - box.width; + } + if (box.y < -margin) { + box.y = -margin; + } else if ((box.y + box.height) > (this.box.height + margin)) { + box.y = this.box.height + margin - box.height; + } + return box; + } + + private setupElements() { + this.svgShape.children().forEach(child => { + this.addElement(child); + }); + const overlappingGroups: ScadaSymbolElement[][] = []; + for (const el of this.elements) { + for (const other of this.elements) { + if (el !== other && el.overlappingCenters(other)) { + let overlappingGroup: ScadaSymbolElement[]; + for (const list of overlappingGroups) { + if (list.includes(other) || list.includes(el)) { + overlappingGroup = list; + break; + } + } + if (!overlappingGroup) { + overlappingGroup = [el, other]; + overlappingGroups.push(overlappingGroup); + } else { + if (!overlappingGroup.includes(el)) { + overlappingGroup.push(el); + } else if (!overlappingGroup.includes(other)){ + overlappingGroup.push(other); + } + } + } + } + } + for (const group of overlappingGroups) { + const centers = group.map(e => e.box.cy); + const center = centers.reduce((a, b) => a + b, 0) / centers.length; + const textElement = group.find(e => e.isText()); + const slots = textElement ? group.length % 2 === 0 ? (group.length + 1) : group.length : group.length; + if (textElement) { + textElement.setInnerTooltipOffset(0, center); + group.splice(group.indexOf(textElement), 1); + } + let offset = - (elementTooltipMinHeight * slots) / 2 + elementTooltipMinHeight / 2; + for (const element of group) { + if (textElement && offset === 0) { + offset += elementTooltipMinHeight; + } + element.setInnerTooltipOffset(offset, center); + offset += elementTooltipMinHeight; + } + } + for (const el of this.elements) { + el.init(); + } + } + + private addElement(e: Element) { + if (hasBBox(e)) { + const scadaSymbolElement = new ScadaSymbolElement(this, e); + this.elements.push(scadaSymbolElement); + e.children().forEach(child => { + if (!(child.type === 'tspan' && e.type === 'text')) { + this.addElement(child); + } + }, true); + } + } + + public destroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + } + + private resize() { + if (this.svgShape) { + const targetWidth = this.rootElement.getBoundingClientRect().width; + const targetHeight = this.rootElement.getBoundingClientRect().height; + let scale: number; + if (targetWidth < targetHeight) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + this.updateHoverFilterStyle(); + this.updateZoomOptions(); + this.updateTooltipPositions(); + } + if (this.performSetup) { + this.performSetup = false; + this.doSetup(); + } + } + } + + private updateHoverFilterStyle() { + if (this.hoverFilterStyle) { + this.hoverFilterStyle.remove(); + } + const whiteBlur = (2.8 / this.scale).toFixed(2); + const blackBlur = (1.2 / this.scale).toFixed(2); + this.hoverFilterStyle = + this.svgShape.style().rule('.hovered', + { + filter: + `drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) + drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) + drop-shadow(0px 0px ${blackBlur}px black)` + } + ); + } + + private updateZoomOptions() { + this.svgShape.panZoom({ + zoomMin: 1, + zoomMax: 4, + zoomFactor: 2 / this.scale + }); + } + + private updateTooltipPositions() { + const container = this.rootElement.getBoundingClientRect(); + for (const e of this.elements) { + e.updateTooltipPosition(container); + } + } + +} + +const hasBBox = (e: Element): boolean => { + try { + if (e.bbox) { + const box = e.bbox(); + return !!box.width || !!box.height; + } else { + return false; + } + } catch (_e) { + return false; + } +}; + +const textTooltip = (el: JQuery, text: string) => { + el.tooltipster({ + theme: ['iot-svg'], + trigger: 'hover', + content: text + }); +}; + +const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => ( + target.left >= container.left - horizontalGap && + target.left + target.width <= container.left + container.width + horizontalGap && + target.top >= container.top - verticalGap && + target.top + target.height <= container.top + container.height + verticalGap +); + +const elementTooltipMinHeight = 36 + 8; +const elementTooltipMinWidth = 100; + +const groupRectStroke = 2; +const groupRectPadding = 2; + +export class ScadaSymbolElement { + + private highlightRect: Rect; + private highlightRectTimeline: Timeline; + + public tooltip: ITooltipsterInstance; + + public tag: string; + + private editing = false; + + private innerTooltipOffset = 0; + + public readonly box: Box; + + private highlighted = false; + + private tooltipMouseX: number; + private tooltipMouseY: number; + + constructor(private editObject: ScadaSymbolEditObject, + public element: Element) { + this.tag = element.attr('tb:tag'); + this.box = element.rbox(this.editObject.svgShape); + } + + public init() { + if (this.isGroup()) { + this.highlightRect = + this.editObject.svgShape + .rect(this.box.width + groupRectPadding * 2, this.box.height + groupRectPadding * 2) + .x(this.box.x - groupRectPadding) + .y(this.box.y - groupRectPadding) + .attr({ + fill: 'none', + rx: this.unscaled(6), + stroke: 'rgba(0, 0, 0, 0.38)', + 'stroke-dasharray': '1', + 'stroke-width': this.unscaled(groupRectStroke), + opacity: 0}); + this.highlightRectTimeline = new Timeline(); + this.highlightRect.timeline(this.highlightRectTimeline); + this.highlightRect.hide(); + } else { + this.element.addClass('tb-element'); + } + this.element.on('mouseenter', (event) => { + this.highlight(); + }); + this.element.on('mouseleave', (event) => { + this.unhighlight(); + }); + if (this.hasTag()) { + this.createTagTooltip(); + } else { + this.createAddTagTooltip(); + } + } + + public overlappingCenters(otherElement: ScadaSymbolElement): boolean { + if (this.isGroup() || otherElement.isGroup()) { + return false; + } + return Math.abs(this.box.cx - otherElement.box.cx) * this.editObject.scale < elementTooltipMinWidth && + Math.abs(this.box.cy - otherElement.box.cy) * this.editObject.scale < elementTooltipMinHeight; + } + + public highlight() { + if (!this.highlighted) { + this.highlighted = true; + if (this.isGroup()) { + this.highlightRectTimeline.finish(); + this.highlightRect + .attr({ + rx: this.unscaled(6), + 'stroke-width': this.unscaled(groupRectStroke) + }); + this.highlightRect.show(); + this.highlightRect.animate(300).attr({opacity: 1}); + } else { + this.element.addClass('hovered'); + } + if (this.hasTag()) { + this.repositionTooltip(); + $(this.tooltip.elementTooltip()).addClass('tb-active'); + } + } + } + + public unhighlight() { + if (this.highlighted) { + this.highlighted = false; + if (this.isGroup()) { + this.highlightRectTimeline.finish(); + this.highlightRect.animate(300).attr({opacity: 0}).after(() => { + this.highlightRect.hide(); + }); + } else { + this.element.removeClass('hovered'); + } + if (this.hasTag() && !this.editing) { + $(this.tooltip.elementTooltip()).removeClass('tb-active'); + } + } + } + + public repositionTooltip() { + const editingElement = this.editObject.editingElement(); + if (!editingElement || editingElement === this) { + this.tooltip.reposition(); + } + } + + public clearTag() { + this.tooltip.destroy(); + this.tag = null; + this.element.attr('tb:tag', null); + this.unhighlight(); + this.createAddTagTooltip(); + } + + public setTag(tag: string) { + this.tooltip.destroy(); + this.tag = tag; + this.element.attr('tb:tag', tag); + this.createTagTooltip(); + } + + public setEditing(editing: boolean) { + this.editing = editing; + if (this.hasTag() && !this.editing && !this.highlighted) { + $(this.tooltip.elementTooltip()).removeClass('tb-active'); + } + } + + public isEditing() { + return this.editing; + } + + public updateTooltipPosition(container: DOMRect) { + if (this.tooltip && !this.tooltip.status().destroyed) { + this.tooltip.reposition(); + const tooltipElement = this.tooltip.elementTooltip(); + if (tooltipElement) { + if (isDomRectContained(tooltipElement.getBoundingClientRect(), container, + elementTooltipMinWidth, elementTooltipMinHeight)) { + tooltipElement.style.visibility = null; + } else { + tooltipElement.style.visibility = 'hidden'; + } + } + } + } + + public setInnerTooltipOffset(offset: number, center: number) { + this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale; + } + + private unscaled(size: number): number { + return size / (this.editObject.scale * this.editObject.svgShape.zoom()); + } + + private scaled(size: number): number { + return size * (this.editObject.scale * this.editObject.svgShape.zoom()); + } + + private createTagTooltip() { + const el = $(this.element.node); + el.tooltipster( + { + zIndex: 100, + arrow: this.isGroup(), + distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6, + theme: ['iot-svg'], + delay: 0, + animationDuration: 0, + interactive: true, + trigger: 'custom', + side: 'top', + trackOrigin: false, + content: '', + functionPosition: (instance, helper, position) => + this.innerTagTooltipPosition(instance, helper, position), + functionReady: (instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.highlight(); + }); + tooltipEl.on('mouseleave', () => { + this.unhighlight(); + }); + } + } + ); + this.tooltip = el.tooltipster('instance'); + this.setupTagPanel(); + } + + private setupTagPanel() { + setupTagPanelTooltip(this, this.editObject.viewContainerRef); + /*this.isEditing = false; + this.unhighlight(); + const tagPanel = + $(`
+ ${this.element.type}: + ${this.tag} + edit + delete +
`); + const updateTagButton = tagPanel.find('.edit-icon'); + textTooltip(updateTagButton, 'Update tag'); + updateTagButton.on('click', () => { + this.setupEditTagPanel(); + }); + const deleteButton = tagPanel.find('.delete-icon'); + textTooltip(deleteButton, 'Remove tag'); + deleteButton.on('click', () => { + this.clearTag(); + }); + this.tooltip.content(tagPanel); + this.tooltip.open();*/ + } + + private setupEditTagPanel() { + this.editing = true; + const editTagInputPanel = + $(`
+ Update tag: + + done + close +
`); + const tagInput = editTagInputPanel.find('input.tag-input'); + const applyTagButton = editTagInputPanel.find('span.apply-icon'); + const closeButton = editTagInputPanel.find('span.close-icon'); + textTooltip(applyTagButton, 'Apply'); + textTooltip(closeButton, 'Cancel'); + tagInput.val(this.tag); + let editPanelClosed = false; + + tagInput.on('keypress', (event) => { + if (event.which === 13) { + const newTag: string = tagInput.val() as string; + if (newTag) { + editPanelClosed = true; + this.setTag(newTag); + } + } + }); + applyTagButton.on('click', () => { + const newTag: string = tagInput.val() as string; + editPanelClosed = true; + if (newTag) { + this.setTag(newTag); + } else { + this.setupTagPanel(); + } + }); + closeButton.on('click', () => { + editPanelClosed = true; + this.setupTagPanel(); + }); + tagInput.on('blur', () => { + setTimeout(() => { + if (!editPanelClosed) { + editPanelClosed = true; + this.setupTagPanel(); + } + }); + }); + this.tooltip.content(editTagInputPanel); + tagInput.trigger('focus'); + } + + private createAddTagTooltip() { + const el = $(this.element.node); + el.tooltipster( + { + zIndex: 100, + arrow: true, + theme: ['iot-svg', 'tb-active'], + delay: [0, 300], + interactive: true, + trigger: 'hover', + side: ['top', 'left', 'bottom', 'right'], + trackOrigin: false, + content: '', + functionBefore: (instance, helper) => { + const mouseEvent = (helper.event as MouseEvent); + this.tooltipMouseX = mouseEvent.clientX; + this.tooltipMouseY = mouseEvent.clientY; + let side: TooltipPositioningSide; + if (this.isGroup()) { + side = 'top'; + } else { + side = this.calculateTooltipSide(helper.origin.getBoundingClientRect(), + mouseEvent.clientX, mouseEvent.clientY); + } + instance.option('side', side); + }, + functionPosition: (instance, helper, position) => + this.innerAddTagTooltipPosition(instance, helper, position), + functionReady: (instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.highlight(); + }); + tooltipEl.on('mouseleave', () => { + this.unhighlight(); + }); + } + } + ); + this.tooltip = el.tooltipster('instance'); + this.setupAddTagPanel(); + } + + private setupAddTagPanel() { + setupAddTagPanelTooltip(this, this.editObject.viewContainerRef); + /*this.isEditing = false; + this.tooltip.off('close'); + this.editObject.dynamicComponentFactoryService.createDynamicComponent( + class TbTooltipComponentInstance extends TbTooltipContent {}, + `
+ ${this.element.type}: + +
`, + [this.editObject.sharedModule] + ).subscribe(componentData => { + const tooltipComponentRef = + this.editObject.viewContainerRef.createComponent(componentData.componentType, + {index: 0, ngModuleRef: componentData.componentModuleRef}); + tooltipComponentRef.instance.componentType = componentData.componentType; + tooltipComponentRef.instance.dynamicComponentFactoryService = this.editObject.dynamicComponentFactoryService; + tooltipComponentRef.instance.addTag = new EventEmitter(); + tooltipComponentRef.instance.addTag.subscribe(() => { + tooltipComponentRef.destroy(); + this.setupAddTagInputPanel(); + }); + const parentElement = tooltipComponentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + this.tooltip.content(content); + });*/ + /* + const addTagPanel = + $(`
+ ${this.element.type}: + +
`); + const addTagButton = addTagPanel.find('.add-tag-button'); + addTagButton.on('click', () => { + this.setupAddTagInputPanel(); + }); + this.tooltip.content(addTagPanel); + */ + // this.tooltip.off('closing'); + } + + private setupAddTagInputPanel() { + this.editing = true; + const addTagInputPanel = + $(`
+ Enter tag: + + done + close +
`); + const tagInput = addTagInputPanel.find('input.tag-input'); + const applyTagButton = addTagInputPanel.find('span.apply-icon'); + const closeButton = addTagInputPanel.find('span.close-icon'); + textTooltip(applyTagButton, 'Apply'); + textTooltip(closeButton, 'Cancel'); + + let addPanelClosed = false; + tagInput.on('keypress', (event) => { + if (event.which === 13) { + const newTag: string = tagInput.val() as string; + if (newTag) { + addPanelClosed = true; + this.tooltip.off('close'); + this.setTag(newTag); + } + } + }); + applyTagButton.on('click', () => { + const newTag: string = tagInput.val() as string; + addPanelClosed = true; + if (newTag) { + this.tooltip.off('close'); + this.setTag(newTag); + } else { + this.unhighlight(); + this.tooltip.close(); + } + }); + closeButton.on('click', () => { + addPanelClosed = true; + this.unhighlight(); + this.tooltip.close(); + }); + tagInput.on('blur', () => { + setTimeout(() => { + if (!addPanelClosed) { + addPanelClosed = true; + this.tooltip.close(); + } + }); + }); + this.tooltip.content(addTagInputPanel); + this.tooltip.option('delay', [0, 10000000]); + this.tooltip.on('close', () => { + this.tooltip.option('delay', [0, 300]); + this.setupAddTagPanel(); + }); + tagInput.trigger('focus'); + } + + private innerTagTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, + position: ITooltipPosition): ITooltipPosition { + const clientRect = helper.origin.getBoundingClientRect(); + if (!this.isGroup()) { + position.coord.top = clientRect.top + (clientRect.height - position.size.height) / 2 + + this.innerTooltipOffset * this.editObject.svgShape.zoom(); + position.coord.left = clientRect.left + (clientRect.width - position.size.width) / 2; + } else { + position.distance = this.scaled(groupRectPadding) + groupRectStroke; + position.coord.top = clientRect.top - position.size.height - (this.scaled(groupRectPadding) + groupRectStroke); + } + return position; + } + + private innerAddTagTooltipPosition(instance: ITooltipsterInstance, + helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { + const distance = 10; + switch (position.side) { + case 'right': + position.coord.top = this.tooltipMouseY - position.size.height / 2; + position.coord.left = this.tooltipMouseX + distance; + position.target = this.tooltipMouseY; + break; + case 'top': + position.coord.top = this.tooltipMouseY - position.size.height - distance; + position.coord.left = this.tooltipMouseX - position.size.width / 2; + position.target = this.tooltipMouseX; + if (this.isGroup()) { + position.coord.top -= elementTooltipMinHeight; + } + break; + case 'left': + position.coord.top = this.tooltipMouseY - position.size.height / 2; + position.coord.left = this.tooltipMouseX - position.size.width - distance; + position.target = this.tooltipMouseY; + break; + case 'bottom': + position.coord.top = this.tooltipMouseY + distance; + position.coord.left = this.tooltipMouseX - position.size.width / 2; + position.target = this.tooltipMouseX; + break; + } + return position; + } + + private calculateTooltipSide(clientRect: DOMRect, mouseX: number, mouseY: number): TooltipPositioningSide { + let side: TooltipPositioningSide; + const cx = clientRect.left + clientRect.width / 2; + const cy = clientRect.top + clientRect.height / 2; + if (clientRect.width > clientRect.height) { + if (Math.abs(cx - mouseX) > clientRect.width / 4) { + if (mouseX < cx) { + side = 'left'; + } else { + side = 'right'; + } + } else { + if (mouseY < cy) { + side = 'top'; + } else { + side = 'bottom'; + } + } + } else { + if (Math.abs(cy - mouseY) > clientRect.height / 4) { + if (mouseY < cy) { + side = 'top'; + } else { + side = 'bottom'; + } + } else { + if (mouseX < cx) { + side = 'left'; + } else { + side = 'right'; + } + } + } + return side; + } + + private hasTag() { + return !!this.tag; + } + + public isGroup() { + return this.element.type === 'g'; + } + + public isText() { + return this.element.type === 'text'; + } + +} + +@Directive() +class TbTooltipContent implements OnDestroy { + + componentType: Type; + dynamicComponentFactoryService: DynamicComponentFactoryService; + + [key: string]: any; + constructor(@TbInject(ElementRef) public element: ElementRef) { + } + + ngOnDestroy(): void { + this.dynamicComponentFactoryService.destroyDynamicComponent(this.componentType); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts index 2ed9a0bb4c..9366321392 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts @@ -19,16 +19,20 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@app/shared/shared.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; +import { ScadaSymbolEditorComponent } from '@home/pages/scada-symbol/scada-symbol-editor.component'; +import { ScadaSymbolTooltipComponentsModule } from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; @NgModule({ declarations: [ + ScadaSymbolEditorComponent, ScadaSymbolComponent ], imports: [ CommonModule, SharedModule, - HomeComponentsModule + HomeComponentsModule, + ScadaSymbolTooltipComponentsModule ] }) export class ScadaSymbolModule { } diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 0c59e74ee5..f741fa5a31 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -466,6 +466,8 @@ mat-icon { background: rgba(0, 0, 0, 0.54); border: 1px solid rgba(0, 0, 0, 0); border-radius: 8px; + display: flex; + align-items: center; .tooltipster-content { padding: 4px 8px; font-size: 12px; @@ -480,7 +482,7 @@ mat-icon { border: 1px solid rgba(0, 0, 0, 0.38); box-shadow: 0 0 10px 6px rgba(0, 0, 0, .2); .tooltipster-content { - color: #000; + color: rgba(0, 0, 0, 0.76); } } .tooltipster-arrow { @@ -549,6 +551,11 @@ mat-icon { } } } + &.tooltipster-left, &.tooltipster-right { + .tooltipster-box { + min-height: 36px; + } + } &.tooltipster-top { .tooltipster-arrow { bottom: -2px; From 64f858a73b7008b518e56627e99b059d178c9244 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 17 May 2024 20:25:51 +0300 Subject: [PATCH 019/331] UI: Improve element tags editing. --- .../scada-symbol-tooltip.components.ts | 42 ++-- .../pages/scada-symbol/scada-symbol.models.ts | 207 ++---------------- 2 files changed, 50 insertions(+), 199 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index cf38552f7a..df9e299cda 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -22,7 +22,7 @@ import { ElementRef, EventEmitter, Input, - NgModule, + NgModule, OnDestroy, Output, Type, ViewChild, @@ -33,6 +33,7 @@ import { ScadaSymbolElement } from '@home/pages/scada-symbol/scada-symbol.models import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { ENTER } from '@angular/cdk/keycodes'; +import Timeout = NodeJS.Timeout; @Directive() abstract class ScadaSymbolPanelComponent implements AfterViewInit { @@ -98,7 +99,7 @@ class ScadaSymbolAddTagPanelComponent extends ScadaSymbolPanelComponent { styleUrls: ['./scada-symbol-tooltip.component.scss'], encapsulation: ViewEncapsulation.None }) -class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit { +class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit, OnDestroy { @ViewChild('tagField') tagField: ElementRef; @@ -117,6 +118,8 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem private closed = false; + private blurTimeout: Timeout; + constructor(public element: ElementRef) { super(element); } @@ -128,6 +131,13 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem }); } + ngOnDestroy() { + if (this.blurTimeout) { + clearTimeout(this.blurTimeout); + this.blurTimeout = null; + } + } + public tagEnter($event: KeyboardEvent) { if ($event.keyCode === ENTER) { $event.preventDefault(); @@ -153,7 +163,7 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem } public onBlur() { - setTimeout(() => { + this.blurTimeout = setTimeout(() => { if (!this.closed) { this.closed = true; this.cancel.emit(); @@ -219,7 +229,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements export class ScadaSymbolTooltipComponentsModule { } export const setupAddTagPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef) => { - symbolElement.setEditing(false); + symbolElement.stopEdit(); symbolElement.tooltip.off('close'); const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolAddTagPanelComponent); componentRef.instance.addTag.subscribe(() => { @@ -229,7 +239,7 @@ export const setupAddTagPanelTooltip = (symbolElement: ScadaSymbolElement, conta }; export const setupTagPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef) => { - symbolElement.setEditing(false); + symbolElement.stopEdit(); symbolElement.unhighlight(); const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolTagPanelComponent); componentRef.instance.updateTag.subscribe(() => { @@ -244,7 +254,17 @@ export const setupTagPanelTooltip = (symbolElement: ScadaSymbolElement, containe }; const setupTagInputPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef, isAdd: boolean) => { - symbolElement.setEditing(true); + + symbolElement.startEdit(() => { + if (isAdd) { + symbolElement.unhighlight(); + symbolElement.tooltip.close(); + } else { + componentRef.destroy(); + setupTagPanelTooltip(symbolElement, container); + } + }); + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolTagInputPanelComponent); componentRef.instance.isAdd = isAdd; @@ -259,13 +279,7 @@ const setupTagInputPanelTooltip = (symbolElement: ScadaSymbolElement, container: symbolElement.setTag(newTag); }); componentRef.instance.cancel.subscribe(() => { - if (isAdd) { - symbolElement.unhighlight(); - symbolElement.tooltip.close(); - } else { - componentRef.destroy(); - setupTagPanelTooltip(symbolElement, container); - } + symbolElement.stopEdit(true); }); if (isAdd) { symbolElement.tooltip.option('delay', [0, 10000000]); @@ -283,7 +297,7 @@ const setTooltipComponent = (symbolElement: componentRef.instance.symbolElement = symbolElement; componentRef.instance.viewInited.subscribe(() => { if (symbolElement.tooltip.status().open) { - symbolElement.repositionTooltip(); + symbolElement.tooltip.reposition(); } }); const parentElement = componentRef.instance.element.nativeElement; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts index d8ee8c1a64..fe843f8cf9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts @@ -71,8 +71,8 @@ export class ScadaSymbolEditObject { this.shapeResize$.observe(this.rootElement); } - public editingElement() { - return this.elements.find(e => e.isEditing()); + public cancelEdit() { + this.elements.filter(e => e.isEditing()).forEach(e => e.stopEdit(true)); } private doSetup() { @@ -300,6 +300,7 @@ export class ScadaSymbolElement { public tag: string; private editing = false; + private onCancelEdit: () => void; private innerTooltipOffset = 0; @@ -373,7 +374,7 @@ export class ScadaSymbolElement { this.element.addClass('hovered'); } if (this.hasTag()) { - this.repositionTooltip(); + this.tooltip.reposition(); $(this.tooltip.elementTooltip()).addClass('tb-active'); } } @@ -396,13 +397,6 @@ export class ScadaSymbolElement { } } - public repositionTooltip() { - const editingElement = this.editObject.editingElement(); - if (!editingElement || editingElement === this) { - this.tooltip.reposition(); - } - } - public clearTag() { this.tooltip.destroy(); this.tag = null; @@ -418,10 +412,24 @@ export class ScadaSymbolElement { this.createTagTooltip(); } - public setEditing(editing: boolean) { - this.editing = editing; - if (this.hasTag() && !this.editing && !this.highlighted) { - $(this.tooltip.elementTooltip()).removeClass('tb-active'); + public startEdit(onCancelEdit: () => void) { + if (!this.editing) { + this.editObject.cancelEdit(); + this.editing = true; + this.onCancelEdit = onCancelEdit; + } + } + + public stopEdit(cancel = false) { + if (this.editing) { + this.editing = false; + if (cancel && this.onCancelEdit) { + this.onCancelEdit(); + } + this.onCancelEdit = null; + if (this.hasTag() && !this.highlighted) { + $(this.tooltip.elementTooltip()).removeClass('tb-active'); + } } } @@ -490,78 +498,6 @@ export class ScadaSymbolElement { private setupTagPanel() { setupTagPanelTooltip(this, this.editObject.viewContainerRef); - /*this.isEditing = false; - this.unhighlight(); - const tagPanel = - $(`
- ${this.element.type}: - ${this.tag} - edit - delete -
`); - const updateTagButton = tagPanel.find('.edit-icon'); - textTooltip(updateTagButton, 'Update tag'); - updateTagButton.on('click', () => { - this.setupEditTagPanel(); - }); - const deleteButton = tagPanel.find('.delete-icon'); - textTooltip(deleteButton, 'Remove tag'); - deleteButton.on('click', () => { - this.clearTag(); - }); - this.tooltip.content(tagPanel); - this.tooltip.open();*/ - } - - private setupEditTagPanel() { - this.editing = true; - const editTagInputPanel = - $(`
- Update tag: - - done - close -
`); - const tagInput = editTagInputPanel.find('input.tag-input'); - const applyTagButton = editTagInputPanel.find('span.apply-icon'); - const closeButton = editTagInputPanel.find('span.close-icon'); - textTooltip(applyTagButton, 'Apply'); - textTooltip(closeButton, 'Cancel'); - tagInput.val(this.tag); - let editPanelClosed = false; - - tagInput.on('keypress', (event) => { - if (event.which === 13) { - const newTag: string = tagInput.val() as string; - if (newTag) { - editPanelClosed = true; - this.setTag(newTag); - } - } - }); - applyTagButton.on('click', () => { - const newTag: string = tagInput.val() as string; - editPanelClosed = true; - if (newTag) { - this.setTag(newTag); - } else { - this.setupTagPanel(); - } - }); - closeButton.on('click', () => { - editPanelClosed = true; - this.setupTagPanel(); - }); - tagInput.on('blur', () => { - setTimeout(() => { - if (!editPanelClosed) { - editPanelClosed = true; - this.setupTagPanel(); - } - }); - }); - this.tooltip.content(editTagInputPanel); - tagInput.trigger('focus'); } private createAddTagTooltip() { @@ -609,105 +545,6 @@ export class ScadaSymbolElement { private setupAddTagPanel() { setupAddTagPanelTooltip(this, this.editObject.viewContainerRef); - /*this.isEditing = false; - this.tooltip.off('close'); - this.editObject.dynamicComponentFactoryService.createDynamicComponent( - class TbTooltipComponentInstance extends TbTooltipContent {}, - `
- ${this.element.type}: - -
`, - [this.editObject.sharedModule] - ).subscribe(componentData => { - const tooltipComponentRef = - this.editObject.viewContainerRef.createComponent(componentData.componentType, - {index: 0, ngModuleRef: componentData.componentModuleRef}); - tooltipComponentRef.instance.componentType = componentData.componentType; - tooltipComponentRef.instance.dynamicComponentFactoryService = this.editObject.dynamicComponentFactoryService; - tooltipComponentRef.instance.addTag = new EventEmitter(); - tooltipComponentRef.instance.addTag.subscribe(() => { - tooltipComponentRef.destroy(); - this.setupAddTagInputPanel(); - }); - const parentElement = tooltipComponentRef.instance.element.nativeElement; - const content = parentElement.firstChild; - parentElement.removeChild(content); - parentElement.style.display = 'none'; - this.tooltip.content(content); - });*/ - /* - const addTagPanel = - $(`
- ${this.element.type}: - -
`); - const addTagButton = addTagPanel.find('.add-tag-button'); - addTagButton.on('click', () => { - this.setupAddTagInputPanel(); - }); - this.tooltip.content(addTagPanel); - */ - // this.tooltip.off('closing'); - } - - private setupAddTagInputPanel() { - this.editing = true; - const addTagInputPanel = - $(`
- Enter tag: - - done - close -
`); - const tagInput = addTagInputPanel.find('input.tag-input'); - const applyTagButton = addTagInputPanel.find('span.apply-icon'); - const closeButton = addTagInputPanel.find('span.close-icon'); - textTooltip(applyTagButton, 'Apply'); - textTooltip(closeButton, 'Cancel'); - - let addPanelClosed = false; - tagInput.on('keypress', (event) => { - if (event.which === 13) { - const newTag: string = tagInput.val() as string; - if (newTag) { - addPanelClosed = true; - this.tooltip.off('close'); - this.setTag(newTag); - } - } - }); - applyTagButton.on('click', () => { - const newTag: string = tagInput.val() as string; - addPanelClosed = true; - if (newTag) { - this.tooltip.off('close'); - this.setTag(newTag); - } else { - this.unhighlight(); - this.tooltip.close(); - } - }); - closeButton.on('click', () => { - addPanelClosed = true; - this.unhighlight(); - this.tooltip.close(); - }); - tagInput.on('blur', () => { - setTimeout(() => { - if (!addPanelClosed) { - addPanelClosed = true; - this.tooltip.close(); - } - }); - }); - this.tooltip.content(addTagInputPanel); - this.tooltip.option('delay', [0, 10000000]); - this.tooltip.on('close', () => { - this.tooltip.option('delay', [0, 300]); - this.setupAddTagPanel(); - }); - tagInput.trigger('focus'); } private innerTagTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, From 9dc76ec308fd08f935de0a73b320b3e66ba28600 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 20 May 2024 18:08:25 +0300 Subject: [PATCH 020/331] UI: Scada symbol editor --- .../widget/lib/svg/iot-svg.models.ts | 40 ++++++ .../scada-symbol-editor.component.ts | 27 ++-- .../scada-symbol-metadata.component.html | 55 ++++++++ .../scada-symbol-metadata.component.scss | 65 +++++++++ .../scada-symbol-metadata.component.ts | 129 ++++++++++++++++++ .../scada-symbol-tooltip.component.scss | 10 ++ .../scada-symbol-tooltip.components.ts | 123 +++++++++++++++-- .../scada-symbol/scada-symbol.component.html | 47 ++++++- .../scada-symbol/scada-symbol.component.ts | 67 +++++++-- .../pages/scada-symbol/scada-symbol.models.ts | 63 +++++++-- .../pages/scada-symbol/scada-symbol.module.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 9 +- 12 files changed, 593 insertions(+), 46 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index db649d8ad1..7277ea0751 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -195,6 +195,46 @@ const updateIotSvgMetadataInDom = (svgDoc: Document, metadata: IotSvgMetadata) = metadataElement.appendChild(cdata); }; +const svgPartsRegex = /()(.*)<\/svg>/gms; + +const svgMetadataRegex = /.*<\/tb:metadata>/gs; + +export const svgRootNodePart = (svgContent: string): string => { + let result = svgContent; + svgPartsRegex.lastIndex = 0; + const match = svgPartsRegex.exec(svgContent); + if (match != null && match.length > 1) { + result = match[1]; + } + return result; +}; + +export const innerSvgContent = (svgContent: string): string => { + let result = svgContent; + svgPartsRegex.lastIndex = 0; + const match = svgPartsRegex.exec(svgContent); + if (match != null && match.length > 2) { + result = match[2]; + } + return result; +}; + +export const stripSvgMetadata = (svgContent: string) => { + let result = svgContent; + svgMetadataRegex.lastIndex = 0; + const match = svgMetadataRegex.exec(svgContent); + if (match !== null && match.length) { + const metadata = match[0]; + result = result.replace(metadata, ''); + } + return result; +}; + +export const innerSvgContentFromSvgjs = (svgContent: string): string => { + const result = innerSvgContent(svgContent); + return result.replace(/svgjs:.*?=".*?"/gs, ''); +}; + const defaultGetValueSettings = (get: IotSvgBehaviorValue): GetValueSettings => ({ action: GetValueAction.DO_NOTHING, defaultValue: get.defaultValue, diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts index 6f73ae7ac4..833f96ea67 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -27,9 +27,12 @@ import { ViewContainerRef, ViewEncapsulation } from '@angular/core'; -import { Subject } from 'rxjs'; import { ScadaSymbolEditObject } from '@home/pages/scada-symbol/scada-symbol.models'; +export interface ScadaSymbolEditorData { + svgContent: string; +} + @Component({ selector: 'tb-scada-symbol-editor', templateUrl: './scada-symbol-editor.component.html', @@ -42,12 +45,10 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI iotSvgShape: ElementRef; @Input() - svgContent: string; + data: ScadaSymbolEditorData; scadaSymbolEditObject: ScadaSymbolEditObject; - private destroy$ = new Subject(); - constructor(private viewContainerRef: ViewContainerRef) { } @@ -57,8 +58,8 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI ngAfterViewInit() { this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.iotSvgShape.nativeElement, this.viewContainerRef); - if (this.svgContent) { - this.scadaSymbolEditObject.setContent(this.svgContent); + if (this.data) { + this.scadaSymbolEditObject.setContent(this.data.svgContent); } } @@ -66,9 +67,11 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'svgContent') { + if (propName === 'data') { if (this.scadaSymbolEditObject) { - this.scadaSymbolEditObject.setContent(this.svgContent); + setTimeout(() => { + this.scadaSymbolEditObject.setContent(this.data.svgContent); + }); } } } @@ -79,5 +82,13 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI this.scadaSymbolEditObject.destroy(); } + get dirty(): boolean { + return this.scadaSymbolEditObject?.dirty; + } + + getContent(): string { + return this.scadaSymbolEditObject?.getContent(); + } + } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.html new file mode 100644 index 0000000000..f4770bd823 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.html @@ -0,0 +1,55 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss new file mode 100644 index 0000000000..48cc8471b8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright ยฉ 2016-2024 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'; + +.tb-scada-symbol-metadata { + display: flex; + flex-direction: column; + gap: 8px; + .tb-scada-symbol-metadata-header { + padding: 24px 24px 0; + display: flex; + gap: 12px; + flex-direction: column-reverse; + align-items: flex-start; + @media #{$mat-gt-sm} { + gap: 0; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + .tb-scada-symbol-metadata-header-components { + width: 100%; + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + } + .tb-scada-symbol-metadata-content { + & > .mat-content { + padding-top: 8px; + @media #{$mat-xs} { + padding-left: 8px; + padding-right: 8px; + } + } + flex: 1; + overflow: auto; + & > div { + padding: 16px; + } + .mat-content { + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + gap: 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts new file mode 100644 index 0000000000..56f0a2d16b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts @@ -0,0 +1,129 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { IotSvgMetadata } from '@home/components/widget/lib/svg/iot-svg.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetConfigMode, widgetType } from '@shared/models/widget.models'; +import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-scada-symbol-metadata', + templateUrl: './scada-symbol-metadata.component.html', + styleUrls: ['./scada-symbol-metadata.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataComponent), + multi: true + } + ] +}) +export class ScadaSymbolMetadataComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + private modelValue: IotSvgMetadata; + + private propagateChange = null; + + public metadataFormGroup: UntypedFormGroup; + + headerOptions: ToggleHeaderOption[] = [ + { + name: this.translate.instant('scada.general'), + value: 'general' + }, + { + name: this.translate.instant('scada.tags'), + value: 'tags' + }, + { + name: this.translate.instant('scada.behavior'), + value: 'behavior' + }, + { + name: this.translate.instant('scada.properties'), + value: 'properties' + } + ]; + + selectedOption = 'general'; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private translate: TranslateService) { + super(store); + } + + ngOnInit(): void { + this.metadataFormGroup = this.fb.group({ + title: [null, [Validators.required]], + stateRenderFunction: [null], + tags: [null], + behavior: [null], + properties: [null] + }); + + this.metadataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.metadataFormGroup.disable({emitEvent: false}); + } else { + this.metadataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: IotSvgMetadata): void { + this.selectedOption = 'general'; + this.modelValue = value; + this.metadataFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + private updateModel() { + const metadata: IotSvgMetadata = this.metadataFormGroup.getRawValue(); + this.modelValue = metadata; + this.propagateChange(this.modelValue); + } + + protected readonly WidgetConfigMode = WidgetConfigMode; + protected readonly widgetType = widgetType; +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss index c7fc243dc0..1ea820b6a2 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss @@ -18,6 +18,16 @@ flex-direction: row; align-items: center; gap: 8px; + &.column { + flex-direction: column; + } + + .tb-confirm-text { + font-size: 14px; + line-height: 16px; + text-align: center; + font-weight: 400; + } .mdc-button { line-height: 20px; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index df9e299cda..faf091c5b7 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -23,6 +23,7 @@ import { EventEmitter, Input, NgModule, OnDestroy, + OnInit, Output, Type, ViewChild, @@ -34,6 +35,8 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { ENTER } from '@angular/cdk/keycodes'; import Timeout = NodeJS.Timeout; +import { MatButton } from '@angular/material/button'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; @Directive() abstract class ScadaSymbolPanelComponent implements AfterViewInit { @@ -55,7 +58,10 @@ abstract class ScadaSymbolPanelComponent implements AfterViewInit { @Component({ template: `
{{ symbolElement?.element?.type }}: - +
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], encapsulation: ViewEncapsulation.None @@ -87,13 +93,13 @@ class ScadaSymbolAddTagPanelComponent extends ScadaSymbolPanelComponent { matTooltipPosition="above" [disabled]="!tag" (click)="onApply()"> - done + done
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], @@ -181,13 +187,13 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem matTooltip="Update tag" matTooltipPosition="above" (click)="onUpdateTag()"> - edit + edit -
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], @@ -195,19 +201,99 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem }) class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit { + @ViewChild('removeTagButton', {read: ElementRef}) + removeTagButton: ElementRef; + @Output() updateTag = new EventEmitter(); @Output() removeTag = new EventEmitter(); - constructor(public element: ElementRef) { + constructor(public element: ElementRef, + private container: ViewContainerRef) { super(element); } + ngAfterViewInit() { + super.ngAfterViewInit(); + setTimeout(() => { + const el = $(this.removeTagButton.nativeElement); + el.tooltipster( + { + zIndex: 200, + arrow: true, + theme: ['iot-svg', 'tb-active'], + interactive: true, + trigger: 'click', + side: 'top', + content: '' + } + ); + const tooltip = el.tooltipster('instance'); + const compRef = + setTooltipComponent(this.symbolElement, this.container, ScadaSymbolRemoveTagConfirmComponent, tooltip); + compRef.instance.removeTag.subscribe(() => { + tooltip.destroy(); + this.removeTag.emit(); + }); + compRef.instance.cancel.subscribe(() => { + tooltip.close(); + }); + tooltip.on('ready', () => { + compRef.instance.yesButton.focus(); + }); + }); + } + public onUpdateTag() { this.updateTag.emit(); } +} + +@Component({ + template: `
+
+
+ + +
+
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + + @ViewChild('yesButton') + yesButton: MatButton; + + deleteText: string; + + @Output() + cancel = new EventEmitter(); + + @Output() + removeTag = new EventEmitter(); + + constructor(public element: ElementRef) { + super(element); + } + + ngOnInit() { + this.deleteText = `Are you sure you want to delete tag
${this.symbolElement?.tag} + from ${this.symbolElement?.element?.type} element?`; + } + + public onCancel() { + this.cancel.emit(); + } public onRemoveTag() { this.removeTag.emit(); @@ -219,7 +305,8 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements [ ScadaSymbolAddTagPanelComponent, ScadaSymbolTagInputPanelComponent, - ScadaSymbolTagPanelComponent + ScadaSymbolTagPanelComponent, + ScadaSymbolRemoveTagConfirmComponent ], imports: [ CommonModule, @@ -292,18 +379,26 @@ const setupTagInputPanelTooltip = (symbolElement: ScadaSymbolElement, container: }; const setTooltipComponent = (symbolElement: ScadaSymbolElement, - container: ViewContainerRef, componentType: Type): ComponentRef => { + container: ViewContainerRef, + componentType: Type, + tooltip?: ITooltipsterInstance): ComponentRef => { + if (!tooltip) { + tooltip = symbolElement.tooltip; + } const componentRef = container.createComponent(componentType); componentRef.instance.symbolElement = symbolElement; componentRef.instance.viewInited.subscribe(() => { - if (symbolElement.tooltip.status().open) { - symbolElement.tooltip.reposition(); + if (tooltip.status().open) { + tooltip.reposition(); } }); + tooltip.on('destroyed', () => { + componentRef.destroy(); + }); const parentElement = componentRef.instance.element.nativeElement; const content = parentElement.firstChild; parentElement.removeChild(content); parentElement.style.display = 'none'; - symbolElement.tooltip.content(content); + tooltip.content(content); return componentRef; }; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index dc4e620f94..fb32b1decc 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -29,13 +29,54 @@ [showCloseDetails]="false" headerHeightPx="64" headerTitle="{{symbolData?.imageResource?.title}}"> -
- {{ metadata | json }} +
+ + + +
- +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index 0d521d0a4f..7b19df3ff3 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, Component, HostBinding, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + HostBinding, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -23,6 +32,12 @@ import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol.models'; import { IotSvgMetadata, parseIotSvgMetadataFromContent } from '@home/components/widget/lib/svg/iot-svg.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { deepClone } from '@core/utils'; +import { + ScadaSymbolEditorComponent, + ScadaSymbolEditorData +} from '@home/pages/scada-symbol/scada-symbol-editor.component'; @Component({ selector: 'tb-scada-symbol', @@ -35,28 +50,43 @@ export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDes @HostBinding('style.width') width = '100%'; @HostBinding('style.height') height = '100%'; + @ViewChild('symbolEditor') + symbolEditor: ScadaSymbolEditorComponent; + symbolData: ScadaSymbolData; + symbolEditorData: ScadaSymbolEditorData; metadata: IotSvgMetadata; + previewMode = false; + + scadaSymbolFormGroup: UntypedFormGroup; + private destroy$ = new Subject(); + private origSymbolData: ScadaSymbolData; + constructor(protected store: Store, private router: Router, - private route: ActivatedRoute) { + private route: ActivatedRoute, + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { super(store); + } + + ngOnInit(): void { + this.scadaSymbolFormGroup = this.fb.group({ + metadata: [null] + }); this.route.data.pipe( takeUntil(this.destroy$) ).subscribe( () => { this.reset(); - this.init(); + this.init(this.route.snapshot.data.symbolData); } ); } - ngOnInit(): void { - } - ngAfterViewInit() { } @@ -66,12 +96,33 @@ export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDes this.destroy$.complete(); } + onApplyScadaSymbolConfig() { + if (this.scadaSymbolFormGroup.valid) { + const svgContent = this.symbolEditor.getContent(); + console.log(svgContent); + } + } + + onRevertScadaSymbolConfig() { + this.init(this.origSymbolData); + } + private reset(): void { + this.previewMode = false; } - private init() { - this.symbolData = this.route.snapshot.data.symbolData; + private init(data: ScadaSymbolData) { + this.origSymbolData = data; + this.symbolData = deepClone(data); + this.symbolEditorData = { + svgContent: this.symbolData.svgContent + }; this.metadata = parseIotSvgMetadataFromContent(this.symbolData.svgContent); + this.scadaSymbolFormGroup.patchValue({ + metadata: this.metadata + }, {emitEvent: false}); + this.scadaSymbolFormGroup.markAsPristine(); + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts index fe843f8cf9..1fc4af8459 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts @@ -30,6 +30,12 @@ import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper; import ITooltipPosition = JQueryTooltipster.ITooltipPosition; +import { + innerSvgContent, + innerSvgContentFromSvgjs, + stripSvgMetadata, + svgRootNodePart +} from '@home/components/widget/lib/svg/iot-svg.models'; export interface ScadaSymbolData { imageResource: ImageResourceInfo; @@ -39,12 +45,15 @@ export interface ScadaSymbolData { export class ScadaSymbolEditObject { public svgShape: Svg; + private svgRootNodePart: string; private box: Box; private elements: ScadaSymbolElement[] = []; private readonly shapeResize$: ResizeObserver; private performSetup = false; private hoverFilterStyle: Style; public scale = 1; + + public dirty = false; constructor(private rootElement: HTMLElement, public viewContainerRef: ViewContainerRef) { this.shapeResize$ = new ResizeObserver(() => { @@ -55,11 +64,17 @@ export class ScadaSymbolEditObject { public setContent(svgContent: string) { this.shapeResize$.unobserve(this.rootElement); if (this.svgShape) { - this.elements.length = 0; + this.destroyElements(); this.svgShape.remove(); } - const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); - this.svgShape = SVG().addTo(this.rootElement).svg(doc.documentElement.innerHTML); + this.scale = 1; + this.dirty = false; + this.svgRootNodePart = svgRootNodePart(svgContent); + svgContent = innerSvgContent(svgContent); + svgContent = stripSvgMetadata(svgContent); + console.log(svgContent); + // const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + this.svgShape = SVG().addTo(this.rootElement).svg(svgContent); this.svgShape.node.style.overflow = 'visible'; this.svgShape.node.style['user-select'] = 'none'; this.box = this.svgShape.bbox(); @@ -71,6 +86,24 @@ export class ScadaSymbolEditObject { this.shapeResize$.observe(this.rootElement); } + public getContent(): string { + if (this.svgShape) { + let svgContent = innerSvgContentFromSvgjs(this.svgShape.svg()); + console.log(svgContent); + svgContent = `${this.svgRootNodePart}\n${svgContent}\n`; + const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + const root = $(doc.documentElement); + root.find('.tb-element').removeClass('tb-element').each((i, e) => { + // + }); + root.find('.tooltipstered').removeClass('tooltipstered'); + svgContent = doc.documentElement.outerHTML; + return svgContent; + } else { + return null; + } + } + public cancelEdit() { this.elements.filter(e => e.isEditing()).forEach(e => e.stopEdit(true)); } @@ -196,6 +229,14 @@ export class ScadaSymbolEditObject { if (this.shapeResize$) { this.shapeResize$.disconnect(); } + this.destroyElements(); + } + + private destroyElements() { + this.elements.forEach(e => { + e.destroy(); + }); + this.elements.length = 0; } private resize() { @@ -269,14 +310,6 @@ const hasBBox = (e: Element): boolean => { } }; -const textTooltip = (el: JQuery, text: string) => { - el.tooltipster({ - theme: ['iot-svg'], - trigger: 'hover', - content: text - }); -}; - const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => ( target.left >= container.left - horizontalGap && target.left + target.width <= container.left + container.width + horizontalGap && @@ -403,6 +436,7 @@ export class ScadaSymbolElement { this.element.attr('tb:tag', null); this.unhighlight(); this.createAddTagTooltip(); + this.editObject.dirty = true; } public setTag(tag: string) { @@ -410,6 +444,7 @@ export class ScadaSymbolElement { this.tag = tag; this.element.attr('tb:tag', tag); this.createTagTooltip(); + this.editObject.dirty = true; } public startEdit(onCancelEdit: () => void) { @@ -456,6 +491,12 @@ export class ScadaSymbolElement { this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale; } + public destroy() { + if (this.tooltip) { + this.tooltip.destroy(); + } + } + private unscaled(size: number): number { return size / (this.editObject.scale * this.editObject.svgShape.zoom()); } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts index 9366321392..36d4cab1a2 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts @@ -21,12 +21,14 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; import { ScadaSymbolEditorComponent } from '@home/pages/scada-symbol/scada-symbol-editor.component'; import { ScadaSymbolTooltipComponentsModule } from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; +import { ScadaSymbolMetadataComponent } from '@home/pages/scada-symbol/scada-symbol-metadata.component'; @NgModule({ declarations: [ ScadaSymbolEditorComponent, - ScadaSymbolComponent + ScadaSymbolComponent, + ScadaSymbolMetadataComponent ], imports: [ CommonModule, 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 265c2c10d2..fbb815fc40 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3410,7 +3410,14 @@ "delete-symbols-title": "Are you sure you want to delete { count, plural, =1 {1 SCADA symbol} other {# SCADA symbols} }?", "delete-symbols-text": "Be careful, after the confirmation all selected SCADA symbols will be removed and all related data will become unrecoverable.", "include-system-symbols": "Include system symbols", - "symbol-preview": "Symbol preview" + "symbol-preview": "Symbol preview", + "general": "General", + "tags": "Tags", + "behavior": "Behavior", + "properties": "Properties", + "title": "Title", + "state-render-function": "State render function", + "preview": "Preview" }, "item": { "selected": "Selected" From 32d34e0acc54a24ecc6068dabb392d49c1506e6a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 21 May 2024 19:26:48 +0300 Subject: [PATCH 021/331] UI: SCADA symbol preview mode. --- ui-ngx/src/app/core/utils.ts | 5 + .../lib/svg/iot-svg-widget.component.html | 2 +- .../lib/svg/iot-svg-widget.component.ts | 39 ++++- .../widget/lib/svg/iot-svg-widget.models.ts | 4 +- .../widget/lib/svg/iot-svg.models.ts | 87 +++++----- .../scada-symbol/scada-symbol.component.html | 44 +++-- .../scada-symbol/scada-symbol.component.ts | 150 ++++++++++++++++-- .../pages/scada-symbol/scada-symbol.models.ts | 63 +++----- 8 files changed, 269 insertions(+), 125 deletions(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 04c0a9fc9b..545f82ddf5 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -229,6 +229,11 @@ export const updateFileContent = (file: File, newContent: string): File => { return new File([blob], file.name, {type: file.type}); }; +export const createFileFromContent = (content: string, name: string, type: string): File => { + const blob = new Blob([content], { type }); + return new File([blob], name, { type }); +}; + const scrollRegex = /(auto|scroll)/; function parentNodes(node: Node, nodes: Node[]): Node[] { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html index b2f4ce1815..ee005b98c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html @@ -22,5 +22,5 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts index c7b5e87723..608feee493 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts @@ -35,8 +35,9 @@ import { iotSvgWidgetDefaultSettings, IotSvgWidgetSettings } from '@home/components/widget/lib/svg/iot-svg-widget.models'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { ImageService } from '@core/http/image.service'; @Component({ selector: 'tb-iot-svg-widget', @@ -63,6 +64,7 @@ export class IotSvgWidgetComponent extends constructor(protected imagePipe: ImagePipe, protected sanitizer: DomSanitizer, private renderer: Renderer2, + private imageService: ImageService, protected cd: ChangeDetectorRef, private http: HttpClient) { super(cd); @@ -75,15 +77,40 @@ export class IotSvgWidgetComponent extends this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); - this.iotSvgObject = new IotSvgObject(this.ctx, this.settings.iotSvg, this.settings.iotSvgObject); + let svgContent$: Observable; + if (this.settings.iotSvgContent) { + svgContent$ = of(this.settings.iotSvgContent); + } else if (this.settings.iotSvgUrl) { + svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl); + } else { + svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'}); + } + + svgContent$.subscribe( + (content) => { + this.initObject(content); + } + ); + } + + private initObject(svgContent: string) { + this.iotSvgObject = new IotSvgObject(this.ctx, svgContent, this.settings.iotSvgObject); this.iotSvgObject.onError((error) => { this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); }); - this.iotSvgObject.init().subscribe(); + this.iotSvgObject.init(); + if (this.iotSvgShape) { + this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); + if (this.autoScale) { + this.onResize(); + } + } } ngAfterViewInit(): void { - this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); + if (this.iotSvgObject) { + this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); + } if (this.autoScale) { this.shapeResize$ = new ResizeObserver(() => { this.onResize(); @@ -114,6 +141,8 @@ export class IotSvgWidgetComponent extends private onResize() { const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width; const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height; - this.iotSvgObject.setSize(shapeWidth, shapeHeight); + if (this.iotSvgObject) { + this.iotSvgObject.setSize(shapeWidth, shapeHeight); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts index e4992ae62f..b3223c5b79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts @@ -18,7 +18,9 @@ import { IotSvgObjectSettings } from '@home/components/widget/lib/svg/iot-svg.mo import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; export interface IotSvgWidgetSettings { - iotSvg: string; + iotSvg?: string; + iotSvgUrl?: string; + iotSvgContent?: string; iotSvgObject: IotSvgObjectSettings; background: BackgroundSettings; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index 7277ea0751..b037b947ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -34,7 +34,7 @@ import { parseFunction } from '@core/utils'; import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs'; -import { map, share } from 'rxjs/operators'; +import { share } from 'rxjs/operators'; import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; @@ -196,45 +196,38 @@ const updateIotSvgMetadataInDom = (svgDoc: Document, metadata: IotSvgMetadata) = }; const svgPartsRegex = /()(.*)<\/svg>/gms; +const tbMetadataRegex = /.*<\/tb:metadata>/gs; -const svgMetadataRegex = /.*<\/tb:metadata>/gs; - -export const svgRootNodePart = (svgContent: string): string => { - let result = svgContent; - svgPartsRegex.lastIndex = 0; - const match = svgPartsRegex.exec(svgContent); - if (match != null && match.length > 1) { - result = match[1]; - } - return result; -}; +export interface IoTSvgContentData { + svgRootNode: string; + innerSvg: string; +} -export const innerSvgContent = (svgContent: string): string => { - let result = svgContent; +export const iotSvgContentData = (svgContent: string): IoTSvgContentData => { + const result: IoTSvgContentData = { + svgRootNode: '', + innerSvg: '' + }; svgPartsRegex.lastIndex = 0; const match = svgPartsRegex.exec(svgContent); - if (match != null && match.length > 2) { - result = match[2]; - } - return result; -}; - -export const stripSvgMetadata = (svgContent: string) => { - let result = svgContent; - svgMetadataRegex.lastIndex = 0; - const match = svgMetadataRegex.exec(svgContent); - if (match !== null && match.length) { - const metadata = match[0]; - result = result.replace(metadata, ''); + if (match != null) { + if (match.length > 1) { + result.svgRootNode = match[1]; + } + if (match.length > 2) { + let innerSvgContent = match[2]; + tbMetadataRegex.lastIndex = 0; + const metadataMatch = tbMetadataRegex.exec(svgContent); + if (metadataMatch !== null && metadataMatch.length) { + const metadata = metadataMatch[0]; + innerSvgContent = innerSvgContent.replace(metadata, ''); + } + result.innerSvg = innerSvgContent; + } } return result; }; -export const innerSvgContentFromSvgjs = (svgContent: string): string => { - const result = innerSvgContent(svgContent); - return result.replace(/svgjs:.*?=".*?"/gs, ''); -}; - const defaultGetValueSettings = (get: IotSvgBehaviorValue): GetValueSettings => ({ action: GetValueAction.DO_NOTHING, defaultValue: get.defaultValue, @@ -335,21 +328,18 @@ export class IotSvgObject { private _onError: (error: string) => void = () => {}; constructor(private ctx: WidgetContext, - private svgPath: string, - private inputSettings: IotSvgObjectSettings) {} - - public init(): Observable { - return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( - map((inputSvgContent) => { - const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); - this.metadata = parseIotSvgMetadataFromDom(doc); - const defaults = defaultIotSvgObjectSettings(this.metadata); - this.settings = mergeDeep({}, defaults, this.inputSettings || {}); - this.prepareMetadata(); - this.prepareSvgShape(doc); - this.initialize(); - }) - ); + private svgContent: string, + private inputSettings: IotSvgObjectSettings) { + } + + public init() { + const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml'); + this.metadata = parseIotSvgMetadataFromDom(doc); + const defaults = defaultIotSvgObjectSettings(this.metadata); + this.settings = mergeDeep({}, defaults, this.inputSettings || {}); + this.prepareMetadata(); + this.prepareSvgShape(doc); + this.initialize(); } public onError(onError: (error: string) => void) { @@ -371,6 +361,9 @@ export class IotSvgObject { this.valueActions.forEach(v => v.destroy()); this.loadingSubject.complete(); this.loadingSubject.unsubscribe(); + if (this.svgShape) { + this.svgShape.remove(); + } } public setSize(targetWidth: number, targetHeight: number) { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index fb32b1decc..b04247b916 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -29,41 +29,39 @@ [showCloseDetails]="false" headerHeightPx="64" headerTitle="{{symbolData?.imageResource?.title}}"> -
+ +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss similarity index 88% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss rename to ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss index 48cc8471b8..bfaf8b0529 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../../../scss/constants'; +@import '../../../../../../scss/constants'; .tb-scada-symbol-metadata { display: flex; @@ -61,5 +61,12 @@ gap: 8px; } } + .tb-form-panel { + .mat-expansion-panel.tb-settings { + & > .mat-expansion-panel-header { + font-weight: 400; + } + } + } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts similarity index 85% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts rename to ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index 56f0a2d16b..966115c44e 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -14,12 +14,15 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, + UntypedFormControl, UntypedFormGroup, + Validator, Validators } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; @@ -39,14 +42,23 @@ import { TranslateService } from '@ngx-translate/core'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ScadaSymbolMetadataComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataComponent), + multi: true } - ] + ], + encapsulation: ViewEncapsulation.None }) -export class ScadaSymbolMetadataComponent extends PageComponent implements OnInit, ControlValueAccessor { +export class ScadaSymbolMetadataComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { @Input() disabled: boolean; + @Input() + tags: string[]; + private modelValue: IotSvgMetadata; private propagateChange = null; @@ -118,6 +130,15 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni ); } + public validate(c: UntypedFormControl) { + const valid = this.metadataFormGroup.valid; + return valid ? null : { + metadata: { + valid: false, + }, + }; + } + private updateModel() { const metadata: IotSvgMetadata = this.metadataFormGroup.getRawValue(); this.modelValue = metadata; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts index 833f96ea67..cd25aa7063 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -17,11 +17,11 @@ import { AfterViewInit, Component, - ElementRef, + ElementRef, EventEmitter, Input, OnChanges, OnDestroy, - OnInit, + OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef, @@ -47,6 +47,9 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI @Input() data: ScadaSymbolEditorData; + @Output() + tags = new EventEmitter(); + scadaSymbolEditObject: ScadaSymbolEditObject; constructor(private viewContainerRef: ViewContainerRef) { @@ -58,6 +61,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI ngAfterViewInit() { this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.iotSvgShape.nativeElement, this.viewContainerRef); + this.scadaSymbolEditObject.tagsUpdated.subscribe(tags => this.tags.emit(tags)); if (this.data) { this.scadaSymbolEditObject.setContent(this.data.svgContent); } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index b04247b916..82b415e570 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -29,14 +29,43 @@ [showCloseDetails]="false" headerHeightPx="64" headerTitle="{{symbolData?.imageResource?.title}}"> - +
+
+ + + + +
+
+
+ + +
+
+
- +
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss index bfaf8b0529..7b923e1688 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss @@ -41,6 +41,7 @@ } } .tb-scada-symbol-metadata-content { + display: flex; & > .mat-content { padding-top: 8px; @media #{$mat-xs} { @@ -60,6 +61,9 @@ @media #{$mat-xs} { gap: 8px; } + &.overflow-hidden { + overflow: hidden; + } } .tb-form-panel { .mat-expansion-panel.tb-settings { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index 966115c44e..d8b6267a8f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -29,9 +29,11 @@ import { PageComponent } from '@shared/components/page.component'; import { IotSvgMetadata } from '@home/components/widget/lib/svg/iot-svg.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { WidgetConfigMode, widgetType } from '@shared/models/widget.models'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; +import { + ScadaSymbolMetadataTagsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; @Component({ selector: 'tb-scada-symbol-metadata', @@ -53,6 +55,9 @@ import { TranslateService } from '@ngx-translate/core'; }) export class ScadaSymbolMetadataComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + @ViewChild('symbolMetadataTags') + symbolMetadataTags: ScadaSymbolMetadataTagsComponent; + @Input() disabled: boolean; @@ -130,6 +135,16 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni ); } + editTagStateRenderFunction(tag: string): void { + this.selectedOption = 'tags'; + this.symbolMetadataTags.editTagStateRenderFunction(tag); + } + + editTagClickAction(tag: string): void { + this.selectedOption = 'tags'; + this.symbolMetadataTags.editTagClickAction(tag); + } + public validate(c: UntypedFormControl) { const valid = this.metadataFormGroup.valid; return valid ? null : { @@ -144,7 +159,4 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni this.modelValue = metadata; this.propagateChange(this.modelValue); } - - protected readonly WidgetConfigMode = WidgetConfigMode; - protected readonly widgetType = widgetType; } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts index cd25aa7063..10c2b3f7f0 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -17,17 +17,17 @@ import { AfterViewInit, Component, - ElementRef, EventEmitter, + ElementRef, Input, OnChanges, OnDestroy, - OnInit, Output, + OnInit, SimpleChanges, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; -import { ScadaSymbolEditObject } from '@home/pages/scada-symbol/scada-symbol.models'; +import { ScadaSymbolEditObject, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol.models'; export interface ScadaSymbolEditorData { svgContent: string; @@ -47,8 +47,8 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI @Input() data: ScadaSymbolEditorData; - @Output() - tags = new EventEmitter(); + @Input() + editObjectCallbacks: ScadaSymbolEditObjectCallbacks; scadaSymbolEditObject: ScadaSymbolEditObject; @@ -60,8 +60,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI ngAfterViewInit() { this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.iotSvgShape.nativeElement, - this.viewContainerRef); - this.scadaSymbolEditObject.tagsUpdated.subscribe(tags => this.tags.emit(tags)); + this.viewContainerRef, this.editObjectCallbacks); if (this.data) { this.scadaSymbolEditObject.setContent(this.data.svgContent); } @@ -86,13 +85,8 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI this.scadaSymbolEditObject.destroy(); } - get dirty(): boolean { - return this.scadaSymbolEditObject?.dirty; - } - getContent(): string { return this.scadaSymbolEditObject?.getContent(); } - } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss index 1ea820b6a2..43613b6d40 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss @@ -21,6 +21,9 @@ &.column { flex-direction: column; } + &.flex-start { + align-items: start; + } .tb-confirm-text { font-size: 14px; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index faf091c5b7..813a652af1 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -189,6 +189,12 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem (click)="onUpdateTag()"> edit + + +
scada.on-click-action
+ + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + + hasStateRenderFunction = false; + + hasClickAction = false; + + constructor(public element: ElementRef) { + super(element); + } + + ngOnInit() { + } + + updateFunctionsState() { + this.hasStateRenderFunction = this.symbolElement.hasStateRenderFunction(); + this.hasClickAction = this.symbolElement.hasClickAction(); + } + + editStateRenderFunction() { + this.symbolElement.editStateRenderFunction(); + } + + editClickAction() { + this.symbolElement.editClickAction(); + } +} + @NgModule({ declarations: [ ScadaSymbolAddTagPanelComponent, ScadaSymbolTagInputPanelComponent, ScadaSymbolTagPanelComponent, - ScadaSymbolRemoveTagConfirmComponent + ScadaSymbolRemoveTagConfirmComponent, + ScadaSymbolTagSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index 82b415e570..449bf95bc7 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -64,6 +64,7 @@
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss new file mode 100644 index 0000000000..d0592c431e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol-metadata-behavior-row { + .tb-id-field { + flex: 1 1 40%; + } + .tb-name-field { + flex: 1 1 40%; + } + .tb-type-field { + flex: 1 1 20%; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts new file mode 100644 index 0000000000..277d9fe4ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -0,0 +1,172 @@ +/// +/// Copyright ยฉ 2016-2024 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 { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { + IotSvgBehavior, + IotSvgBehaviorAction, + IotSvgBehaviorType, + iotSvgBehaviorTypes, + iotSvgBehaviorTypeTranslations, + IotSvgBehaviorValue +} from '@home/components/widget/lib/svg/iot-svg.models'; + +export const behaviorValid = (behavior: IotSvgBehavior): boolean => { + if (!behavior.id || !behavior.name || !behavior.type) { + return false; + } + switch (behavior.type) { + case IotSvgBehaviorType.value: + const valueBehavior = behavior as IotSvgBehaviorValue; + if (!valueBehavior.valueType) { + return false; + } + break; + case IotSvgBehaviorType.action: + const actionBehavior = behavior as IotSvgBehaviorAction; + if (!actionBehavior.valueToDataType) { + return false; + } + break; + case IotSvgBehaviorType.widgetAction: + break; + } + return true; +}; + +export const behaviorValidator = (control: AbstractControl): ValidationErrors | null => { + const behavior: IotSvgBehavior = control.value; + if (!behaviorValid(behavior)) { + return { + behavior: true + }; + } + return null; +}; + +@Component({ + selector: 'tb-scada-symbol-metadata-behavior-row', + templateUrl: './scada-symbol-behavior-row.component.html', + styleUrls: ['./scada-symbol-behavior-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit { + + @ViewChild('idInput') + idInput: ElementRef; + + iotSvgBehaviorTypes = iotSvgBehaviorTypes; + iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations; + + @Input() + disabled: boolean; + + @Output() + behaviorRemoved = new EventEmitter(); + + behaviorRowFormGroup: UntypedFormGroup; + + modelValue: IotSvgBehavior; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.behaviorRowFormGroup = this.fb.group({ + id: [null, []], + name: [null, []], + type: [null, []] + }); + this.behaviorRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.behaviorRowFormGroup.disable({emitEvent: false}); + } else { + this.behaviorRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: IotSvgBehavior): void { + this.modelValue = value; + this.behaviorRowFormGroup.patchValue( + { + id: value?.id, + name: value?.name, + type: value?.type + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + editBehavior() { + + } + + focus() { + this.idInput.nativeElement.scrollIntoView(); + this.idInput.nativeElement.focus(); + } + + private updateModel() { + const value: IotSvgBehavior = this.behaviorRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html new file mode 100644 index 0000000000..28efaca269 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html @@ -0,0 +1,60 @@ + +
+
+
+
scada.behavior.id
+
scada.behavior.name
+
scada.behavior.type
+
+
+
+
+ + +
+ +
+
+
+
+
+ +
+
+ + + {{ 'scada.behavior.no-behaviors' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss new file mode 100644 index 0000000000..a9fd4df110 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss @@ -0,0 +1,43 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol-behaviors { + flex: 1; + margin: 12px; + .tb-form-table-header-cell { + &.tb-id-header { + flex: 1 1 40%; + } + &.tb-name-header { + flex: 1 1 40%; + } + &.tb-type-header { + flex: 1 1 20%; + } + &.tb-actions-header { + width: 120px; + min-width: 120px; + } + } + .tb-form-table { + overflow: hidden; + } + .tb-form-table-body { + overflow: auto; + tb-scada-symbol-metadata-behavior-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts new file mode 100644 index 0000000000..d06e790784 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts @@ -0,0 +1,177 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + forwardRef, + HostBinding, + Input, + OnInit, + QueryList, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { IotSvgBehavior, IotSvgBehaviorType } from '@home/components/widget/lib/svg/iot-svg.models'; +import { ValueType } from '@shared/models/constants'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { + behaviorValid, + behaviorValidator, + ScadaSymbolBehaviorRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component'; + +@Component({ + selector: 'tb-scada-symbol-metadata-behaviors', + templateUrl: './scada-symbol-behaviors.component.html', + styleUrls: ['./scada-symbol-behaviors.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnInit, Validator { + + @HostBinding('style.display') styleDisplay = 'flex'; + @HostBinding('style.overflow') styleOverflow = 'hidden'; + + @ViewChildren(ScadaSymbolBehaviorRowComponent) + behaviorRows: QueryList; + + @Input() + disabled: boolean; + + behaviorsFormGroup: UntypedFormGroup; + + get dragEnabled(): boolean { + return this.behaviorsFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.behaviorsFormGroup = this.fb.group({ + behaviors: this.fb.array([]) + }); + this.behaviorsFormGroup.valueChanges.subscribe( + () => { + let behaviors: IotSvgBehavior[] = this.behaviorsFormGroup.get('behaviors').value; + if (behaviors) { + behaviors = behaviors.filter(b => behaviorValid(b)); + } + this.propagateChange(behaviors); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.behaviorsFormGroup.disable({emitEvent: false}); + } else { + this.behaviorsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: IotSvgBehavior[] | undefined): void { + const behaviors= value || []; + this.behaviorsFormGroup.setControl('behaviors', this.prepareBehaviorsFormArray(behaviors), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.behaviorsFormGroup.valid; + return valid ? null : { + behaviors: { + valid: false, + }, + }; + } + + behaviorDrop(event: CdkDragDrop) { + const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + const behavior = behaviorsArray.at(event.previousIndex); + behaviorsArray.removeAt(event.previousIndex); + behaviorsArray.insert(event.currentIndex, behavior); + } + + behaviorsFormArray(): UntypedFormArray { + return this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + } + + trackByBehavior(index: number, behaviorControl: AbstractControl): any { + return behaviorControl; + } + + removeBehavior(index: number, emitEvent = true) { + (this.behaviorsFormGroup.get('behaviors') as UntypedFormArray).removeAt(index, {emitEvent}); + } + + addBehavior() { + const behavior: IotSvgBehavior = { + id: '', + name: '', + type: IotSvgBehaviorType.value, + valueType: ValueType.BOOLEAN, + defaultValue: false + }; + const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + const behaviorControl = this.fb.control(behavior, [behaviorValidator]); + behaviorsArray.push(behaviorControl); + setTimeout(() => { + const behaviorRow = this.behaviorRows.get(this.behaviorRows.length-1); + behaviorRow.focus(); + }); + } + + private prepareBehaviorsFormArray(behaviors: IotSvgBehavior[] | undefined): UntypedFormArray { + const behaviorsControls: Array = []; + if (behaviors) { + behaviors.forEach((behavior) => { + behaviorsControls.push(this.fb.control(behavior, [behaviorValidator])); + }); + } + return this.fb.array(behaviorsControls); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts index 5b86c23f04..469f7e9782 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts @@ -24,13 +24,21 @@ import { ScadaSymbolMetadataComponent } from '@home/pages/scada-symbol/metadata- import { ScadaSymbolMetadataTagsComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; +import { + ScadaSymbolBehaviorsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component'; +import { + ScadaSymbolBehaviorRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component'; @NgModule({ declarations: [ ScadaSymbolMetadataComponent, ScadaSymbolMetadataTagComponent, - ScadaSymbolMetadataTagsComponent + ScadaSymbolMetadataTagsComponent, + ScadaSymbolBehaviorsComponent, + ScadaSymbolBehaviorRowComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html index e3e0b82d82..01914d1e32 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -22,7 +22,7 @@
scada.tag
- + {{ tag }} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index f074955776..663a87d8fa 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -37,6 +37,7 @@ import { import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models'; import { MatExpansionPanel } from '@angular/material/expansion'; import { JsFuncComponent } from '@shared/components/js-func.component'; +import { MatSelect } from '@angular/material/select'; @Component({ selector: 'tb-scada-symbol-metadata-tag', @@ -53,6 +54,9 @@ import { JsFuncComponent } from '@shared/components/js-func.component'; }) export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, OnInit, OnChanges { + @ViewChild('tagSelect') + tagSelect: MatSelect; + @ViewChild('expansionPanel') expansionPanel: MatExpansionPanel; @@ -163,6 +167,11 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On }); } + focus() { + this.tagSelect._elementRef.nativeElement.scrollIntoView(); + this.tagSelect.focus(); + } + private openPanelWithCallback(panel: MatExpansionPanel, callback: () => void) { if (!panel.expanded) { const s = panel.afterExpand.subscribe(() => { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts index 13bebfe72d..5b6c8210db 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts @@ -164,6 +164,10 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O addTag() { this.addNewTag(null); + setTimeout(() => { + const tagComponent = this.metadataTags.get(this.metadataTags.length-1); + tagComponent.focus(); + }); } editTagStateRenderFunction(tag: string): void { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index 16573951a7..395f6c67f0 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -58,5 +58,10 @@ formControlName="tags">
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index d8b6267a8f..96f4b0b184 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -80,7 +80,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni value: 'tags' }, { - name: this.translate.instant('scada.behavior'), + name: this.translate.instant('scada.behavior.behavior'), value: 'behavior' }, { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index d3b08fd795..c595f63071 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -218,13 +218,22 @@ export class ScadaSymbolComponent extends PageComponent enterPreviewMode() { this.symbolData.svgContent = this.prepareSvgContent(); - this.previewIotSvgObjectSettings = {}; + this.previewIotSvgObjectSettings = { + behavior: {}, + properties: {} + }; this.scadaPreviewFormGroup.patchValue({ iotSvgObject: this.previewIotSvgObjectSettings }, {emitEvent: false}); this.scadaPreviewFormGroup.markAsPristine(); const settings: IotSvgWidgetSettings = {...iotSvgWidgetDefaultSettings, - ...{iotSvg: null, iotSvgContent: this.symbolData.svgContent, iotSvgObject: this.previewIotSvgObjectSettings}}; + ...{ + simulated: true, + iotSvg: null, + iotSvgContent: this.symbolData.svgContent, + iotSvgObject: this.previewIotSvgObjectSettings + } + }; this.previewWidget = { typeFullFqn: 'system.iot_svg', type: widgetType.rpc, 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 f6a81e8f1d..8ac12e376e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3389,9 +3389,6 @@ "error-entities": "There was an error creating {{count}} entities." } }, - "iot-svg": { - "behavior": "Behavior" - }, "scada": { "symbol": "SCADA symbol", "symbols": "SCADA symbols", @@ -3413,7 +3410,6 @@ "symbol-preview": "Symbol preview", "general": "General", "tags": "Tags", - "behavior": "Behavior", "properties": "Properties", "title": "Title", "state-render-function": "State render function", @@ -3423,7 +3419,21 @@ "remove-tag": "Remove tag", "add-tag": "Add tag", "no-tags": "No tags configured", - "select-tag": "Select tag" + "select-tag": "Select tag", + "preview-widget-action-text": "Widget action '{{type}}' successfully invoked!", + "behavior": { + "behavior": "Behavior", + "id": "Id", + "name": "Name", + "type": "Type", + "no-behaviors": "No behaviors configured", + "add-behavior": "Add behavior", + "type-action": "Action", + "type-value": "Value", + "type-widget-action": "Widget action", + "behavior-settings": "Behavior settings", + "remove-behavior": "Remove behavior" + } }, "item": { "selected": "Selected" diff --git a/ui-ngx/src/assets/widget/svg/drawing.svg b/ui-ngx/src/assets/widget/svg/drawing.svg index 55d60d04c1..5f64fc6643 100644 --- a/ui-ngx/src/assets/widget/svg/drawing.svg +++ b/ui-ngx/src/assets/widget/svg/drawing.svg @@ -28,7 +28,7 @@ "tag": "onButton", "actions": { "click": { - "actionFunction": "ctx.api.callAction(event, 'onUpdateState');" + "actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});" } } }, @@ -36,7 +36,7 @@ "tag": "offButton", "actions": { "click": { - "actionFunction": "ctx.api.callAction(event, 'offUpdateState');" + "actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n" } } }, @@ -59,33 +59,30 @@ ], "behavior": [ { - "id": "levelState", + "id": "level", "name": "Level", "type": "value", "valueType": "DOUBLE", - "defaultValue": 0, - "valueId": "level" + "defaultValue": 0 }, { - "id": "disabledState", + "id": "disabled", "name": "{i18n:widgets.rpc-state.disabled-state}", "hint": "{i18n:widgets.rpc-state.disabled-state-hint}", "stateLabel": "{i18n:widgets.rpc-state.disabled}", "type": "value", "valueType": "BOOLEAN", - "defaultValue": false, - "valueId": "disabled" + "defaultValue": false }, { - "id": "onState", + "id": "on", "name": "On/Off state", "trueLabel": "{i18n:widgets.rpc-state.on}", "falseLabel": "{i18n:widgets.rpc-state.off}", "stateLabel": "{i18n:widgets.rpc-state.on}", "type": "value", "valueType": "BOOLEAN", - "defaultValue": false, - "valueId": "on" + "defaultValue": false }, { "id": "onUpdateState", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 18f5f6755d..88d0df0e29 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -564,6 +564,7 @@ .tb-form-table-header { height: 48px; + min-height: 48px; border-bottom: 1px solid rgba(0, 0, 0, 0.12); &-cell { color: rgba(0, 0, 0, 0.54); From 2e4e580036a3ededbd73d31f75460d7f4573cfa9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 24 May 2024 19:39:37 +0300 Subject: [PATCH 025/331] UI: SCADA symbol: Implement behavior settings. Improve iot svg widget initialization. --- .../iot-svg-object-settings.component.html | 12 +- .../svg/iot-svg-object-settings.component.ts | 10 +- .../svg/iot-svg-object-settings.models.ts | 4 +- .../lib/svg/iot-svg-widget.component.ts | 96 +-- .../widget/lib/svg/iot-svg.models.ts | 160 +++-- ...scada-symbol-behavior-panel.component.html | 138 ++++ ...scada-symbol-behavior-panel.component.scss | 67 ++ .../scada-symbol-behavior-panel.component.ts | 148 +++++ .../scada-symbol-behavior-row.component.html | 3 +- .../scada-symbol-behavior-row.component.ts | 106 +++- .../scada-symbol-behaviors.component.ts | 10 +- ...scada-symbol-metadata-components.module.ts | 6 +- .../scada-symbol-metadata-tags.component.html | 2 +- .../pages/scada-symbol/scada-symbol.models.ts | 37 +- .../assets/locale/locale.constant-en_US.json | 26 +- ui-ngx/src/assets/widget/svg/drawing.svg | 589 +++++++++--------- 16 files changed, 945 insertions(+), 469 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html index 1e9bd9616d..3e0684fd75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html @@ -60,35 +60,35 @@
{{ property.subLabel | customTranslate }}
- +
{{ property.fieldSuffix | customTranslate }}
- - - +
{{ property.fieldSuffix | customTranslate }}
- - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts index 58d4346e53..7073d034ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts @@ -30,13 +30,13 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { defaultIotSvgObjectSettings, - parseIotSvgMetadataFromContent, IotSvgBehaviorType, IotSvgMetadata, - IotSvgObjectSettings + IotSvgObjectSettings, + IotSvgPropertyType, + parseIotSvgMetadataFromContent } from '@home/components/widget/lib/svg/iot-svg.models'; import { HttpClient } from '@angular/common/http'; -import { ValueType } from '@shared/models/constants'; import { IAliasController } from '@core/api/widget-api.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; @@ -69,6 +69,8 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control IotSvgBehaviorType = IotSvgBehaviorType; + IotSvgPropertyType = IotSvgPropertyType; + @Input() disabled: boolean; @@ -205,7 +207,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control if (property.required) { validators.push(Validators.required); } - if (property.type === 'number') { + if (property.type === IotSvgPropertyType.number) { if (isDefinedAndNotNull(property.min)) { validators.push(Validators.min(property.min)); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts index ebb8b908a9..807dffcc56 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models'; +import { IotSvgProperty, IotSvgPropertyType } from '@home/components/widget/lib/svg/iot-svg.models'; export interface IotSvgPropertyRow { label: string; @@ -35,7 +35,7 @@ export const toPropertyRows = (properties: IotSvgProperty[]): IotSvgPropertyRow[ }; result.push(propertyRow); } - if (property.type === 'switch') { + if (property.type === IotSvgPropertyType.switch) { propertyRow.switch = property; } else { propertyRow.properties.push(property); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts index fc443bcd39..52f40152a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts @@ -19,18 +19,18 @@ import { ChangeDetectorRef, Component, ElementRef, + Input, OnDestroy, OnInit, Renderer2, + TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; -import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; import { HttpClient } from '@angular/common/http'; -import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models'; -import { ResizeObserver } from '@juggle/resize-observer'; +import { IotSvgObject, IotSvgObjectCallbacks } from '@home/components/widget/lib/svg/iot-svg.models'; import { iotSvgWidgetDefaultSettings, IotSvgWidgetSettings @@ -40,6 +40,7 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi import { ImageService } from '@core/http/image.service'; import { WidgetComponent } from '@home/components/widget/widget.component'; import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { WidgetContext } from '@home/models/widget-component.models'; @Component({ selector: 'tb-iot-svg-widget', @@ -47,110 +48,79 @@ import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'], encapsulation: ViewEncapsulation.None }) -export class IotSvgWidgetComponent extends - BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { +export class IotSvgWidgetComponent implements OnInit, AfterViewInit, OnDestroy, IotSvgObjectCallbacks { @ViewChild('iotSvgShape', {static: false}) iotSvgShape: ElementRef; + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + private settings: IotSvgWidgetSettings; + private svgContent$: Observable; backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; iotSvgObject: IotSvgObject; - private autoScale = true; - - private shapeResize$: ResizeObserver; - constructor(public widgetComponent: WidgetComponent, protected imagePipe: ImagePipe, protected sanitizer: DomSanitizer, - private renderer: Renderer2, private imageService: ImageService, protected cd: ChangeDetectorRef, private http: HttpClient) { - super(cd); } ngOnInit(): void { - super.ngOnInit(); + this.ctx.$scope.actionWidget = this; this.settings = mergeDeep({} as IotSvgWidgetSettings, iotSvgWidgetDefaultSettings, this.ctx.settings || {}); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); - let svgContent$: Observable; if (this.settings.iotSvgContent) { - svgContent$ = of(this.settings.iotSvgContent); + this.svgContent$ = of(this.settings.iotSvgContent); } else if (this.settings.iotSvgUrl) { - svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl); + this.svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl); } else { - svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'}); - } - - svgContent$.subscribe( - (content) => { - this.initObject(content); - } - ); - } - - private initObject(svgContent: string) { - const simulated = this.ctx.utilsService.widgetEditMode || - this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false); - this.iotSvgObject = new IotSvgObject(this.ctx, svgContent, this.settings.iotSvgObject, simulated); - this.iotSvgObject.onError((error) => { - this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); - }); - this.iotSvgObject.onMessage((message) => { - this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true); - }); - this.iotSvgObject.init(); - if (this.iotSvgShape) { - this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); - if (this.autoScale) { - this.onResize(); - } + this.svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'}); } } ngAfterViewInit(): void { - if (this.iotSvgObject) { - this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); - } - if (this.autoScale) { - this.shapeResize$ = new ResizeObserver(() => { - this.onResize(); - }); - this.shapeResize$.observe(this.iotSvgShape.nativeElement); - this.onResize(); - } - super.ngAfterViewInit(); + this.svgContent$.subscribe((content) => { + this.initObject(this.iotSvgShape.nativeElement, content); + }); } ngOnDestroy() { - if (this.shapeResize$) { - this.shapeResize$.disconnect(); - } if (this.iotSvgObject) { this.iotSvgObject.destroy(); } - super.ngOnDestroy(); } public onInit() { - super.onInit(); const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; this.cd.detectChanges(); } - private onResize() { - const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width; - const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height; - if (this.iotSvgObject) { - this.iotSvgObject.setSize(shapeWidth, shapeHeight); - } + onSvgObjectError(error: string) { + this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); } + + onSvgObjectMessage(message: string) { + this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true); + } + + private initObject(rootElement: HTMLElement, + svgContent: string) { + const simulated = this.ctx.utilsService.widgetEditMode || + this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false); + this.iotSvgObject = new IotSvgObject(rootElement, this.ctx, svgContent, this.settings.iotSvgObject, this, simulated); + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index 1302afefd1..5c463b7165 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -15,7 +15,7 @@ /// import { ValueType } from '@shared/models/constants'; -import { Box, Element, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js'; +import { Box, Element, Runner, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js'; import '@svgdotjs/svg.panzoom.js'; import { DataToValueType, @@ -33,7 +33,7 @@ import { mergeDeep, parseFunction } from '@core/utils'; -import { BehaviorSubject, forkJoin, Observable, Observer, of } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs'; import { share } from 'rxjs/operators'; import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -41,6 +41,7 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; +import { ResizeObserver } from '@juggle/resize-observer'; export interface IotSvgApi { formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; @@ -112,14 +113,37 @@ export interface IotSvgBehaviorValue extends IotSvgBehaviorBase { } export interface IotSvgBehaviorAction extends IotSvgBehaviorBase { + valueType: ValueType; valueToDataType: ValueToDataType; constantValue: any; valueToDataFunction: string; } -export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction; +export type IotSvgBehavior = IotSvgBehaviorValue & IotSvgBehaviorAction; + +export enum IotSvgPropertyType { + text = 'text', + number = 'number', + switch = 'switch', + color = 'color', + color_settings = 'color_settings', + font = 'font', + units = 'units' +} + +export const iotSvgPropertyTypes = Object.keys(IotSvgPropertyType) as IotSvgPropertyType[]; -export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; +export const iotSvgPropertyTypeTranslations = new Map( + [ + [IotSvgPropertyType.text, 'scada.property.type-text'], + [IotSvgPropertyType.number, 'scada.property.type-number'], + [IotSvgPropertyType.switch, 'scada.property.type-switch'], + [IotSvgPropertyType.color, 'scada.property.type-color'], + [IotSvgPropertyType.color_settings, 'scada.property.type-color-settings'], + [IotSvgPropertyType.font, 'scada.property.type-font'], + [IotSvgPropertyType.units, 'scada.property.type-units'] + ] +); export interface IotSvgPropertyBase { id: string; @@ -320,17 +344,19 @@ export type IotSvgObjectSettings = { const parseError = (ctx: WidgetContext, err: any): string => ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; +export interface IotSvgObjectCallbacks { + onSvgObjectError: (error: string) => void; + onSvgObjectMessage: (message: string) => void; +} + export class IotSvgObject { private metadata: IotSvgMetadata; private settings: IotSvgObjectSettings; private context: IotSvgContext; - private rootElement: HTMLElement; private svgShape: Svg; private box: Box; - private targetWidth: number; - private targetHeight: number; private loadingSubject = new BehaviorSubject(false); private valueGetters: ValueGetter[] = []; @@ -339,19 +365,22 @@ export class IotSvgObject { private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; - loading$ = this.loadingSubject.asObservable().pipe(share()); + private readonly shapeResize$: ResizeObserver; + private scale = 1; - private _onError: (error: string) => void = () => {}; + private performInit = true; - private _onMessage: (message: string) => void = () => {}; + loading$ = this.loadingSubject.asObservable().pipe(share()); - constructor(private ctx: WidgetContext, + constructor(private rootElement: HTMLElement, + private ctx: WidgetContext, private svgContent: string, private inputSettings: IotSvgObjectSettings, + private callbacks: IotSvgObjectCallbacks, private simulated: boolean) { - } - - public init() { + this.shapeResize$ = new ResizeObserver(() => { + this.resize(); + }); const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml'); this.metadata = parseIotSvgMetadataFromDom(doc); const defaults = defaultIotSvgObjectSettings(this.metadata); @@ -359,25 +388,13 @@ export class IotSvgObject { defaults, this.inputSettings || {} as IotSvgObjectSettings); this.prepareMetadata(); this.prepareSvgShape(doc); - this.initialize(); - } - - public onError(onError: (error: string) => void) { - this._onError = onError; - } - - public onMessage(onMessage: (message: string) => void) { - this._onMessage = onMessage; - } - - public addTo(element: HTMLElement) { - this.rootElement = element; - if (this.svgShape) { - this.svgShape.addTo(element); - } + this.shapeResize$.observe(this.rootElement); } public destroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } for (const stateValueId of Object.keys(this.stateValueSubjects)) { this.stateValueSubjects[stateValueId].complete(); this.stateValueSubjects[stateValueId].unsubscribe(); @@ -385,16 +402,15 @@ export class IotSvgObject { this.valueActions.forEach(v => v.destroy()); this.loadingSubject.complete(); this.loadingSubject.unsubscribe(); - if (this.svgShape) { - this.svgShape.remove(); + for (const tag of this.metadata.tags) { + const elements = this.context.tags[tag.tag]; + elements.forEach(element => { + element.timeline().finish(); + element.timeline(null); + }); } - } - - public setSize(targetWidth: number, targetHeight: number) { - this.targetWidth = targetWidth; - this.targetHeight = targetHeight; if (this.svgShape) { - this.resize(); + this.svgShape.remove(); } } @@ -421,15 +437,10 @@ export class IotSvgObject { this.svgShape.node.style['user-select'] = 'none'; this.box = this.svgShape.bbox(); this.svgShape.size(this.box.width, this.box.height); - if (this.rootElement) { - this.svgShape.addTo(this.rootElement); - } - if (this.targetWidth && this.targetHeight) { - this.resize(); - } + this.svgShape.addTo(this.rootElement); } - private initialize() { + private init() { this.context = { api: { formatValue, @@ -447,7 +458,7 @@ export class IotSvgObject { }; const taggedElements = this.svgShape.find(`[tb\\:tag]`); for (const element of taggedElements) { - const tag = element.attr('tb:tag'); + const tag: string = element.attr('tb:tag'); let elements = this.context.tags[tag]; if (!elements) { elements = []; @@ -489,7 +500,7 @@ export class IotSvgObject { next: (val) => {this.onValue(getBehavior.id, val);}, error: (err) => { const message = parseError(this.ctx, err); - this._onError(message); + this.onError(message); } }, this.simulated); this.valueGetters.push(valueGetter); @@ -526,6 +537,14 @@ export class IotSvgObject { } } + private onError(error: string) { + this.callbacks.onSvgObjectError(error); + } + + private onMessage(message: string) { + this.callbacks.onSvgObjectMessage(message); + } + private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial>) { const behavior = this.metadata.behavior.find(b => b.id === behaviorId); if (behavior) { @@ -547,7 +566,7 @@ export class IotSvgObject { observer.error(err); } const message = parseError(this.ctx, err); - this._onError(message); + this.onError(message); } } ); @@ -557,22 +576,38 @@ export class IotSvgObject { if (this.simulated) { const translatedType = this.ctx.translate.instant(widgetActionTypeTranslationMap.get(widgetAction.type)); const message = this.ctx.translate.instant('scada.preview-widget-action-text', {type: translatedType}); - this._onMessage(message); + this.onMessage(message); } else { this.ctx.actionsApi.onWidgetAction(event, widgetAction); } + if (observer?.next) { + observer.next(); + } } } } private resize() { - let scale: number; - if (this.targetWidth < this.targetHeight) { - scale = this.targetWidth / this.box.width; - } else { - scale = this.targetHeight / this.box.height; + if (this.svgShape) { + const targetWidth = this.rootElement.getBoundingClientRect().width; + const targetHeight = this.rootElement.getBoundingClientRect().height; + if (targetWidth && targetHeight) { + let scale: number; + if (targetWidth < targetHeight) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + } + if (this.performInit) { + this.performInit = false; + this.init(); + } + } } - this.svgShape.node.style.transform = `scale(${scale})`; } private onValue(id: string, value: any) { @@ -598,7 +633,7 @@ export class IotSvgObject { private renderState(): void { this.metadata.stateRender(this.svgShape, this.context); for (const tag of this.metadata.tags) { - const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); + const elements = this.context.tags[tag.tag];// this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); elements.forEach(element => { tag.stateRender(element, this.context); }); @@ -664,6 +699,7 @@ export class IotSvgObject { private animate(element: Element, duration: number): Runner { element.timeline().finish(); + element.timeline(new Timeline()); return element.animate(duration, 0, 'now'); } @@ -692,9 +728,9 @@ export class IotSvgObject { if (property) { const value = this.settings.properties[id]; if (isDefinedAndNotNull(value)) { - if (property.type === 'color-settings') { + if (property.type === IotSvgPropertyType.color_settings) { return ColorProcessor.fromSettings(value); - } else if (property.type === 'string') { + } else if (property.type === IotSvgPropertyType.text) { const result = this.ctx.utilsService.customTranslation(value, value); const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); return createLabelFromSubscriptionEntityInfo(entityInfo, result); @@ -702,13 +738,13 @@ export class IotSvgObject { return value; } else { switch (property.type) { - case 'string': + case IotSvgPropertyType.text: return ''; - case 'number': + case IotSvgPropertyType.number: return 0; - case 'color': + case IotSvgPropertyType.color: return '#000'; - case 'color-settings': + case IotSvgPropertyType.color_settings: return ColorProcessor.fromSettings(constantColor('#000')); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html new file mode 100644 index 0000000000..12d308ed7c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html @@ -0,0 +1,138 @@ + +
+
{{ panelTitle | translate }}
+
+
+
scada.behavior.id
+ + + +
+
+
scada.behavior.name
+ + + +
+
+
scada.behavior.hint
+ + + +
+
+
scada.behavior.type
+ + + + {{ iotSvgBehaviorTypeTranslations.get(type) | translate }} + + + +
+
+
scada.behavior.value-type
+ + + +
+ + {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} +
+
+ + + {{ valueTypesMap.get(valueType).name | translate }} + +
+
+
+ +
+
scada.behavior.default-value
+ +
+ +
+
scada.behavior.true-label
+ + + +
+
+
scada.behavior.false-label
+ + + +
+
+
scada.behavior.state-label
+ + + +
+
+
+ +
+
+
{{ 'scada.behavior.default-payload' | translate }}
+ + {{ 'widgets.value-action.converter-value' | translate }} + {{ 'widgets.value-action.converter-constant' | translate }} + {{ 'widgets.value-action.converter-function' | translate }} + {{ 'widgets.value-action.converter-none' | translate }} + +
+ + + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss new file mode 100644 index 0000000000..3fbbd245a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright ยฉ 2016-2024 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'; + +.tb-scada-symbol-behavior-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-behavior-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + .mat-mdc-form-field.tb-value-type { + .mat-mdc-form-field-infix { + max-height: 40px; + } + mat-select-trigger { + .tb-value-type-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 10px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + vertical-align: bottom; + } + } + } + } + } + .tb-scada-symbol-behavior-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-behavior-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts new file mode 100644 index 0000000000..bdb2827dac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { merge } from 'rxjs'; +import { + IotSvgBehavior, + IotSvgBehaviorType, + iotSvgBehaviorTypes, + iotSvgBehaviorTypeTranslations +} from '@app/modules/home/components/widget/lib/svg/iot-svg.models'; +import { ValueType, valueTypesMap } from '@shared/models/constants'; +import { ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-scada-symbol-behavior-panel', + templateUrl: './scada-symbol-behavior-panel.component.html', + styleUrls: ['./scada-symbol-behavior-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorPanelComponent implements OnInit { + + IotSvgBehaviorType = IotSvgBehaviorType; + + ValueType = ValueType; + + ValueToDataType = ValueToDataType; + + iotSvgBehaviorTypes = iotSvgBehaviorTypes; + iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations; + + valueTypes = Object.keys(ValueType) as ValueType[]; + + valueTypesMap = valueTypesMap; + + @Input() + isAdd = false; + + @Input() + behavior: IotSvgBehavior; + + @Input() + popover: TbPopoverComponent; + + @Output() + behaviorSettingsApplied = new EventEmitter(); + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + panelTitle: string; + + behaviorFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService) { + } + + ngOnInit(): void { + this.panelTitle = this.isAdd ? 'scada.behavior.add-behavior' : 'scada.behavior.behavior-settings'; + this.behaviorFormGroup = this.fb.group( + { + id: [this.behavior.id, [Validators.required]], + name: [this.behavior.name, [Validators.required]], + hint: [this.behavior.hint, []], + type: [this.behavior.type, [Validators.required]], + valueType: [this.behavior.valueType, [Validators.required]], + defaultValue: [this.behavior.defaultValue, [Validators.required]], + trueLabel: [this.behavior.trueLabel, []], + falseLabel: [this.behavior.falseLabel, []], + stateLabel: [this.behavior.stateLabel, []], + valueToDataType: [this.behavior.valueToDataType, [Validators.required]], + constantValue: [this.behavior.constantValue, [Validators.required]], + valueToDataFunction: [this.behavior.valueToDataFunction, [Validators.required]] + } + ); + merge(this.behaviorFormGroup.get('type').valueChanges, + this.behaviorFormGroup.get('valueType').valueChanges, + this.behaviorFormGroup.get('valueToDataType').valueChanges).subscribe(() => { + this.updateValidators(); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyBehaviorSettings() { + const behavior = this.behaviorFormGroup.getRawValue(); + this.behaviorSettingsApplied.emit(behavior); + } + + private updateValidators() { + const type: IotSvgBehaviorType = this.behaviorFormGroup.get('type').value; + const valueType: ValueType = this.behaviorFormGroup.get('valueType').value; + let valueToDataType: ValueToDataType = this.behaviorFormGroup.get('valueToDataType').value; + this.behaviorFormGroup.disable({emitEvent: false}); + this.behaviorFormGroup.get('id').enable({emitEvent: false}); + this.behaviorFormGroup.get('name').enable({emitEvent: false}); + this.behaviorFormGroup.get('type').enable({emitEvent: false}); + this.behaviorFormGroup.get('hint').enable({emitEvent: false}); + switch (type) { + case IotSvgBehaviorType.value: + this.behaviorFormGroup.get('valueType').enable({emitEvent: false}); + this.behaviorFormGroup.get('defaultValue').enable({emitEvent: false}); + if (valueType === ValueType.BOOLEAN) { + this.behaviorFormGroup.get('trueLabel').enable({emitEvent: false}); + this.behaviorFormGroup.get('falseLabel').enable({emitEvent: false}); + this.behaviorFormGroup.get('stateLabel').enable({emitEvent: false}); + } + break; + case IotSvgBehaviorType.action: + if (valueType === ValueType.BOOLEAN && valueToDataType === ValueToDataType.VALUE) { + this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.CONSTANT}, {emitEvent: false}); + valueToDataType = ValueToDataType.CONSTANT; + } else if (valueType !== ValueType.BOOLEAN && valueToDataType === ValueToDataType.CONSTANT) { + this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.VALUE}, {emitEvent: false}); + valueToDataType = ValueToDataType.VALUE; + } + this.behaviorFormGroup.get('valueType').enable({emitEvent: false}); + this.behaviorFormGroup.get('valueToDataType').enable({emitEvent: false}); + if (valueToDataType === ValueToDataType.CONSTANT) { + this.behaviorFormGroup.get('constantValue').enable({emitEvent: false}); + } else if (valueToDataType === ValueToDataType.FUNCTION) { + this.behaviorFormGroup.get('valueToDataFunction').enable({emitEvent: false}); + } + break; + case IotSvgBehaviorType.widgetAction: + break; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html index 5b9c4d2cf9..766cce736b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html @@ -32,7 +32,8 @@
-
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts index c34da44d23..3cb586218b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts @@ -74,13 +74,14 @@ export class ScadaSymbolEditObject { this.scale = 1; const contentData = iotSvgContentData(svgContent); this.svgRootNodePart = contentData.svgRootNode; - this.svgShape = SVG().addTo(this.rootElement).svg(contentData.innerSvg); + this.svgShape = SVG().svg(contentData.innerSvg); this.svgShape.node.style.overflow = 'visible'; this.svgShape.node.style['user-select'] = 'none'; this.box = this.svgShape.bbox(); this.svgShape.size(this.box.width, this.box.height); this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); this.svgShape.style().attr('tb:inner', true).rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); + this.svgShape.addTo(this.rootElement); this.updateHoverFilterStyle(); this.performSetup = true; this.shapeResize$.observe(this.rootElement); @@ -245,22 +246,24 @@ export class ScadaSymbolEditObject { if (this.svgShape) { const targetWidth = this.rootElement.getBoundingClientRect().width; const targetHeight = this.rootElement.getBoundingClientRect().height; - let scale: number; - if (targetWidth < targetHeight) { - scale = targetWidth / this.box.width; - } else { - scale = targetHeight / this.box.height; - } - if (this.scale !== scale) { - this.scale = scale; - this.svgShape.node.style.transform = `scale(${this.scale})`; - this.updateHoverFilterStyle(); - this.updateZoomOptions(); - this.updateTooltipPositions(); - } - if (this.performSetup) { - this.performSetup = false; - this.doSetup(); + if (targetWidth && targetHeight) { + let scale: number; + if (targetWidth < targetHeight) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + this.updateHoverFilterStyle(); + this.updateZoomOptions(); + this.updateTooltipPositions(); + } + if (this.performSetup) { + this.performSetup = false; + this.doSetup(); + } } } } 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 8ac12e376e..42c5a644c2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3432,7 +3432,31 @@ "type-value": "Value", "type-widget-action": "Widget action", "behavior-settings": "Behavior settings", - "remove-behavior": "Remove behavior" + "remove-behavior": "Remove behavior", + "hint": "Hint", + "value-type": "Value type", + "default-value": "Default value", + "true-label": "True label", + "false-label": "False label", + "state-label": "State label", + "default-payload": "Default payload" + }, + "property": { + "property": "Property", + "id": "Id", + "name": "Name", + "type": "Type", + "type-text": "Text", + "type-number": "Number", + "type-switch": "Switch", + "type-color": "Color", + "type-color-settings": "Color settings", + "type-font": "Font", + "type-units": "Units", + "no-properties": "No properties configured", + "add-property": "Add property", + "property-settings": "Property settings", + "remove-property": "Remove property" } }, "item": { diff --git a/ui-ngx/src/assets/widget/svg/drawing.svg b/ui-ngx/src/assets/widget/svg/drawing.svg index 5f64fc6643..23759d17a5 100644 --- a/ui-ngx/src/assets/widget/svg/drawing.svg +++ b/ui-ngx/src/assets/widget/svg/drawing.svg @@ -1,307 +1,310 @@ - - { - "title": "My first IoT SVG Object", - "stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}", - "tags": [ - { - "tag": "level", - "stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});" - }, - { - "tag": "levelTitle", - "stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}" - }, - { - "tag": "levelValue", - "stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}", - "actions": { - "click": { - "actionFunction": "ctx.api.callAction(event, 'levelValueClick');" - } - } - }, - { - "tag": "fan", - "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n" - }, - { - "tag": "onButton", - "actions": { - "click": { - "actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});" - } - } - }, - { - "tag": "offButton", - "actions": { - "click": { - "actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n" - } - } - }, - { - "tag": "levelUpButton", - "actions": { - "click": { - "actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" - } - } - }, - { - "tag": "levelDownButton", - "actions": { - "click": { - "actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" - } - } +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" version="1.1" viewBox="0 0 100 100"> +<tb:metadata xmlns=""><![CDATA[{ + "title": "Level and Fan12", + "stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}", + "tags": [ + { + "tag": "level", + "stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});" + }, + { + "tag": "levelTitle", + "stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}" + }, + { + "tag": "levelValue", + "stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'levelValueClick');" } - ], - "behavior": [ - { - "id": "level", - "name": "Level", - "type": "value", - "valueType": "DOUBLE", - "defaultValue": 0 - }, - { - "id": "disabled", - "name": "{i18n:widgets.rpc-state.disabled-state}", - "hint": "{i18n:widgets.rpc-state.disabled-state-hint}", - "stateLabel": "{i18n:widgets.rpc-state.disabled}", - "type": "value", - "valueType": "BOOLEAN", - "defaultValue": false - }, - { - "id": "on", - "name": "On/Off state", - "trueLabel": "{i18n:widgets.rpc-state.on}", - "falseLabel": "{i18n:widgets.rpc-state.off}", - "stateLabel": "{i18n:widgets.rpc-state.on}", - "type": "value", - "valueType": "BOOLEAN", - "defaultValue": false - }, - { - "id": "onUpdateState", - "name": "{i18n:widgets.rpc-state.turn-on}", - "hint": "{i18n:widgets.rpc-state.turn-on-hint}", - "type": "action", - "valueType": "BOOLEAN", - "valueToDataType": "CONSTANT", - "constantValue": true - }, - { - "id": "offUpdateState", - "name": "{i18n:widgets.rpc-state.turn-off}", - "hint": "{i18n:widgets.rpc-state.turn-off-hint}", - "type": "action", - "valueType": "BOOLEAN", - "valueToDataType": "CONSTANT", - "constantValue": false - }, - { - "id": "levelUpdateState", - "name": "Update level", - "type": "action", - "valueType": "DOUBLE", - "valueToDataType": "VALUE", - "constantValue": 0 - }, - { - "id": "levelValueClick", - "name": "On level value click", - "type": "widgetAction" + } + }, + { + "tag": "fan", + "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n var level = ctx.values.level; \n var minLevel = ctx.properties.minLevel; \n var maxLevel = ctx.properties.maxLevel;\n\n var speed = (level - minLevel) / (maxLevel - minLevel);\n speed = Math.max(0, Math.min(1, speed))*2; \n\n var t = element.timeline();\n if (!t.active()) {\n ctx.api.animate(element, 1000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n t.speed(speed);\n} else {\n element.timeline().stop();\n}\n", + "actions": null + }, + { + "tag": "onButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});\n\n" } - ], - "properties": [ - { - "id": "showLevelTitle", - "name": "Level title", - "type": "switch", - "default": true, - "rowClass": "column-xs" - }, - { - "id": "levelTitle", - "name": "Level title", - "type": "string", - "default": "{i18n:widgets.battery-level.value}", - "disableOnProperty": "showLevelTitle", - "fieldClass": "flex" - }, - { - "id": "levelTitleFont", - "name": "Level title", - "type": "font", - "default": { - "size": 6, - "sizeUnit": "px", - "family": "Roboto", - "weight": "normal", - "style": "normal" - }, - "disableOnProperty": "showLevelTitle" - }, - { - "id": "levelTitleColor", - "name": "Level title", - "type": "color", - "default": "#000000", - "disableOnProperty": "showLevelTitle" - }, - { - "id": "showValue", - "name": "Value", - "type": "switch", - "default": true, - "rowClass": "column-xs" - }, - { - "id": "valueUnits", - "name": "Value", - "type": "units", - "default": "", - "disableOnProperty": "showValue", - "fieldClass": "flex" - }, - { - "id": "valueDecimals", - "name": "Value", - "type": "number", - "default": 2, - "min": 0, - "max": 15, - "step": 1, - "fieldSuffix": "decimals", - "disableOnProperty": "showValue", - "fieldClass": "flex" - }, - { - "id": "valueFont", - "name": "Value", - "type": "font", - "default": { - "size": 6, - "sizeUnit": "px", - "family": "Roboto", - "weight": "normal", - "style": "normal" - }, - "disableOnProperty": "showValue" - }, - { - "id": "valueColor", - "name": "Value", - "type": "color", - "default": "#000000", - "disableOnProperty": "showValue" - }, - { - "id": "minLevel", - "name": "Level range", - "subLabel": "min", - "type": "number", - "required": true, - "default": 0, - "min": 0, - "rowClass": "column-xs", - "fieldClass": "flex-xs" - }, - { - "id": "maxLevel", - "name": "Level range", - "subLabel": "max", - "type": "number", - "required": true, - "default": 100, - "min": 0, - "fieldClass": "flex-xs" - }, - { - "id": "showMinMaxLevel", - "name": "Min/Max label", - "type": "switch", - "default": true - }, - { - "id": "minMaxLevelFont", - "name": "Min/Max label", - "type": "font", - "default": { - "size": 6, - "sizeUnit": "px", - "family": "Roboto", - "weight": "normal", - "style": "normal" - }, - "disableOnProperty": "showMinMaxLevel" - }, - { - "id": "minMaxLevelColor", - "name": "Min/Max label", - "type": "color", - "default": "#666", - "disableOnProperty": "showMinMaxLevel" - }, - { - "id": "levelBackground", - "name": "Level background", - "subLabel": "Enabled", - "type": "color-settings", - "default": { - "type": "constant", - "color": "#1abb48", - "colorFunction": "return value &gt; 70 ? '#d5280d' : '#1abb48';" - }, - "divider": true, - "rowClass": "column-xs" - }, - { - "id": "disabledLevelBackground", - "name": "Level background", - "subLabel": "Disabled", - "type": "color", - "default": "#ccc" - } - ] - } - - - - - min - - + } + }, + { + "tag": "offButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n" + } + } + }, + { + "tag": "levelUpButton", + "actions": { + "click": { + "actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" + } + } + }, + { + "tag": "levelDownButton", + "actions": { + "click": { + "actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" + } + } + }, + { + "tag": "ramka", + "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n if (!element.timeline().active()) {\n ctx.api.animate(element, 1000).scale(1.1).loop(0, true);\n }\n} else {\n element.timeline().stop();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "level", + "name": "Level", + "hint": null, + "type": "value", + "valueType": "DOUBLE", + "defaultValue": 10, + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "valueToDataType": null, + "constantValue": null, + "valueToDataFunction": null + }, + { + "id": "disabled", + "name": "{i18n:widgets.rpc-state.disabled-state}", + "hint": "{i18n:widgets.rpc-state.disabled-state-hint}", + "type": "value", + "valueType": "BOOLEAN", + "defaultValue": false, + "trueLabel": "", + "falseLabel": "", + "stateLabel": "{i18n:widgets.rpc-state.disabled}", + "valueToDataType": null, + "constantValue": null, + "valueToDataFunction": null + }, + { + "id": "on", + "name": "On/Off state", + "hint": null, + "type": "value", + "valueType": "BOOLEAN", + "defaultValue": true, + "trueLabel": "{i18n:widgets.rpc-state.on}", + "falseLabel": "{i18n:widgets.rpc-state.off}", + "stateLabel": "{i18n:widgets.rpc-state.on}", + "valueToDataType": null, + "constantValue": null, + "valueToDataFunction": null + }, + { + "id": "onUpdateState", + "name": "{i18n:widgets.rpc-state.turn-on}", + "hint": "{i18n:widgets.rpc-state.turn-on-hint}", + "type": "action", + "valueType": "BOOLEAN", + "valueToDataType": "CONSTANT", + "constantValue": true + }, + { + "id": "offUpdateState", + "name": "{i18n:widgets.rpc-state.turn-off}", + "hint": "{i18n:widgets.rpc-state.turn-off-hint}", + "type": "action", + "valueType": "BOOLEAN", + "valueToDataType": "CONSTANT", + "constantValue": false + }, + { + "id": "levelUpdateState", + "name": "Update level", + "type": "action", + "valueType": "DOUBLE", + "valueToDataType": "VALUE", + "constantValue": 0 + }, + { + "id": "levelValueClick", + "name": "On level value click", + "type": "widgetAction" + } + ], + "properties": [ + { + "id": "showLevelTitle", + "name": "Level title", + "type": "switch", + "default": true, + "rowClass": "column-xs" + }, + { + "id": "levelTitle", + "name": "Level title", + "type": "text", + "default": "{i18n:widgets.battery-level.value}", + "disableOnProperty": "showLevelTitle", + "fieldClass": "flex" + }, + { + "id": "levelTitleFont", + "name": "Level title", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showLevelTitle" + }, + { + "id": "levelTitleColor", + "name": "Level title", + "type": "color", + "default": "#000000", + "disableOnProperty": "showLevelTitle" + }, + { + "id": "showValue", + "name": "Value", + "type": "switch", + "default": true, + "rowClass": "column-xs" + }, + { + "id": "valueUnits", + "name": "Value", + "type": "units", + "default": "", + "disableOnProperty": "showValue", + "fieldClass": "flex" + }, + { + "id": "valueDecimals", + "name": "Value", + "type": "number", + "default": 2, + "min": 0, + "max": 15, + "step": 1, + "fieldSuffix": "decimals", + "disableOnProperty": "showValue", + "fieldClass": "flex" + }, + { + "id": "valueFont", + "name": "Value", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showValue" + }, + { + "id": "valueColor", + "name": "Value", + "type": "color", + "default": "#000000", + "disableOnProperty": "showValue" + }, + { + "id": "minLevel", + "name": "Level range", + "subLabel": "min", + "type": "number", + "required": true, + "default": 0, + "min": 0, + "rowClass": "column-xs", + "fieldClass": "flex-xs" + }, + { + "id": "maxLevel", + "name": "Level range", + "subLabel": "max", + "type": "number", + "required": true, + "default": 100, + "min": 0, + "fieldClass": "flex-xs" + }, + { + "id": "showMinMaxLevel", + "name": "Min/Max label", + "type": "switch", + "default": true + }, + { + "id": "minMaxLevelFont", + "name": "Min/Max label", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showMinMaxLevel" + }, + { + "id": "minMaxLevelColor", + "name": "Min/Max label", + "type": "color", + "default": "#666", + "disableOnProperty": "showMinMaxLevel" + }, + { + "id": "levelBackground", + "name": "Level background", + "subLabel": "Enabled", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1abb48", + "colorFunction": "return value > 70 ? '#d5280d' : '#1abb48';" + }, + "divider": true, + "rowClass": "column-xs" + }, + { + "id": "disabledLevelBackground", + "name": "Level background", + "subLabel": "Disabled", + "type": "color", + "default": "#ccc" + } + ] +}]]> + min N/A Level max - - - - + On - - + Off - - + - - + - + \ No newline at end of file From 1aca952eca39dedfb134f96de41aaf646edd5d60 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 27 May 2024 18:17:23 +0300 Subject: [PATCH 026/331] UI: Implement SCADA symbol properties section. --- .../widget/lib/svg/iot-svg.models.ts | 6 + ...scada-symbol-behavior-panel.component.html | 2 +- .../scada-symbol-behavior-row.component.ts | 70 ++++- .../scada-symbol-behaviors.component.html | 2 + .../scada-symbol-behaviors.component.ts | 36 ++- ...scada-symbol-metadata-components.module.ts | 22 +- .../scada-symbol-metadata.component.html | 5 + .../scada-symbol-metadata.component.ts | 1 - .../scada-symbol-properties.component.html | 63 ++++ .../scada-symbol-properties.component.scss | 43 +++ .../scada-symbol-properties.component.ts | 207 +++++++++++++ ...scada-symbol-property-panel.component.html | 184 ++++++++++++ ...scada-symbol-property-panel.component.scss | 49 +++ .../scada-symbol-property-panel.component.ts | 129 ++++++++ .../scada-symbol-property-row.component.html | 49 +++ .../scada-symbol-property-row.component.scss | 26 ++ .../scada-symbol-property-row.component.ts | 280 ++++++++++++++++++ .../scada-symbol/scada-symbol.component.ts | 3 + .../assets/locale/locale.constant-en_US.json | 19 +- 19 files changed, 1168 insertions(+), 28 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index 5c463b7165..c77065fbf8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -145,6 +145,12 @@ export const iotSvgPropertyTypeTranslations = new Mapscada.behavior.value-type
- +
{{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts index 1fba319cb2..12bc0d2ecc 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -30,11 +30,11 @@ import { } from '@angular/core'; import { AbstractControl, - ControlValueAccessor, + ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormBuilder, + UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - ValidationErrors, + ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; import { @@ -50,6 +50,9 @@ import { ScadaSymbolBehaviorPanelComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component'; import { ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { + ScadaSymbolBehaviorsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component'; export const behaviorValid = (behavior: IotSvgBehavior): boolean => { if (!behavior.id || !behavior.name || !behavior.type) { @@ -80,16 +83,6 @@ export const behaviorValid = (behavior: IotSvgBehavior): boolean => { return true; }; -export const behaviorValidator = (control: AbstractControl): ValidationErrors | null => { - const behavior: IotSvgBehavior = control.value; - if (!behaviorValid(behavior)) { - return { - behavior: true - }; - } - return null; -}; - @Component({ selector: 'tb-scada-symbol-metadata-behavior-row', templateUrl: './scada-symbol-behavior-row.component.html', @@ -99,11 +92,16 @@ export const behaviorValidator = (control: AbstractControl): ValidationErrors | provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true } ], encapsulation: ViewEncapsulation.None }) -export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit { +export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit, Validator { @ViewChild('idInput') idInput: ElementRef; @@ -117,6 +115,9 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On @Input() disabled: boolean; + @Input() + index: number; + @Output() behaviorRemoved = new EventEmitter(); @@ -130,12 +131,13 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On private cd: ChangeDetectorRef, private popoverService: TbPopoverService, private renderer: Renderer2, - private viewContainerRef: ViewContainerRef) { + private viewContainerRef: ViewContainerRef, + private behaviorsComponent: ScadaSymbolBehaviorsComponent) { } ngOnInit() { this.behaviorRowFormGroup = this.fb.group({ - id: [null, [Validators.required]], + id: [null, [this.behaviorIdValidator()]], name: [null, [Validators.required]], type: [null, [Validators.required]] }); @@ -223,6 +225,42 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On this.editBehavior(null, this.editButton, true, onCanceled); } + public validate(c: UntypedFormControl) { + const idControl = this.behaviorRowFormGroup.get('id'); + if (idControl.hasError('behaviorIdNotUnique')) { + idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); + } + if (idControl.hasError('behaviorIdNotUnique')) { + this.behaviorRowFormGroup.get('id').markAsTouched(); + return { + behaviorIdNotUnique: true + }; + } + const behavior: IotSvgBehavior = {...this.modelValue, ...this.behaviorRowFormGroup.value}; + if (!behaviorValid(behavior)) { + return { + behavior: true + }; + } + return null; + } + + private behaviorIdValidator(): ValidatorFn { + return control => { + if (!control.value) { + return { + required: true + }; + } + if (!this.behaviorsComponent.behaviorIdUnique(control.value, this.index)) { + return { + behaviorIdNotUnique: true + }; + } + return null; + }; + } + private onTypeChanged(newType: IotSvgBehaviorType) { const prevType = this.modelValue.type; this.modelValue = {...this.modelValue, ...{type: newType}}; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html index 28efaca269..7d5991ed4b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html @@ -31,6 +31,7 @@ class="tb-draggable-form-table-row" *ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;"> @@ -46,6 +47,7 @@
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index 96f4b0b184..75e6fcc8bb 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -128,7 +128,6 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni } writeValue(value: IotSvgMetadata): void { - this.selectedOption = 'general'; this.modelValue = value; this.metadataFormGroup.patchValue( value, {emitEvent: false} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html new file mode 100644 index 0000000000..c0f3316bbe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html @@ -0,0 +1,63 @@ + +
+
+
+
scada.property.id
+
scada.property.name
+
scada.property.type
+
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ + + {{ 'scada.property.no-properties' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss new file mode 100644 index 0000000000..9bafbf2ccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss @@ -0,0 +1,43 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol-properties { + flex: 1; + margin: 12px; + .tb-form-table-header-cell { + &.tb-id-header { + flex: 1 1 40%; + } + &.tb-name-header { + flex: 1 1 40%; + } + &.tb-type-header { + flex: 1 1 20%; + } + &.tb-actions-header { + width: 120px; + min-width: 120px; + } + } + .tb-form-table { + overflow: hidden; + } + .tb-form-table-body { + overflow: auto; + tb-scada-symbol-metadata-property-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts new file mode 100644 index 0000000000..a36536464f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + forwardRef, + HostBinding, + Input, + OnInit, + QueryList, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { IotSvgProperty, IotSvgPropertyType } from '@home/components/widget/lib/svg/iot-svg.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { + propertyValid, + ScadaSymbolPropertyRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-scada-symbol-metadata-properties', + templateUrl: './scada-symbol-properties.component.html', + styleUrls: ['./scada-symbol-properties.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnInit, Validator { + + @HostBinding('style.display') styleDisplay = 'flex'; + @HostBinding('style.overflow') styleOverflow = 'hidden'; + + @ViewChildren(ScadaSymbolPropertyRowComponent) + propertyRows: QueryList; + + @Input() + disabled: boolean; + + booleanPropertyIds: string[] = []; + + propertiesFormGroup: UntypedFormGroup; + + errorText = ''; + + get dragEnabled(): boolean { + return this.propertiesFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService) { + } + + ngOnInit() { + this.propertiesFormGroup = this.fb.group({ + properties: this.fb.array([]) + }); + this.propertiesFormGroup.valueChanges.subscribe( + () => { + let properties: IotSvgProperty[] = this.propertiesFormGroup.get('properties').value; + if (properties) { + properties = properties.filter(p => propertyValid(p)); + } + this.booleanPropertyIds = properties.filter(p => p.type === IotSvgPropertyType.switch).map(p => p.id); + this.propagateChange(properties); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.propertiesFormGroup.disable({emitEvent: false}); + } else { + this.propertiesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: IotSvgProperty[] | undefined): void { + const properties= value || []; + this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: false}); + this.booleanPropertyIds = properties.filter(p => p.type === IotSvgPropertyType.switch).map(p => p.id); + } + + public validate(c: UntypedFormControl) { + this.errorText = ''; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const notUniqueControls = + propertiesArray.controls.filter(control => control.hasError('propertyIdNotUnique')); + for (const control of notUniqueControls) { + control.updateValueAndValidity({onlySelf: false, emitEvent: false}); + if (control.hasError('propertyIdNotUnique')) { + this.errorText = this.translate.instant('scada.property.not-unique-property-ids-error'); + } + } + const valid = this.propertiesFormGroup.valid; + return valid ? null : { + properties: { + valid: false, + }, + }; + } + + public propertyIdUnique(id: string, index: number): boolean { + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + for (let i = 0; i < propertiesArray.controls.length; i++) { + if (i !== index) { + const otherControl = propertiesArray.controls[i]; + if (id === otherControl.value.id) { + return false; + } + } + } + return true; + } + + propertyDrop(event: CdkDragDrop) { + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const property = propertiesArray.at(event.previousIndex); + propertiesArray.removeAt(event.previousIndex); + propertiesArray.insert(event.currentIndex, property); + } + + propertiesFormArray(): UntypedFormArray { + return this.propertiesFormGroup.get('properties') as UntypedFormArray; + } + + trackByProperty(index: number, propertyControl: AbstractControl): any { + return propertyControl; + } + + removeProperty(index: number, emitEvent = true) { + (this.propertiesFormGroup.get('properties') as UntypedFormArray).removeAt(index, {emitEvent}); + } + + addProperty() { + const property: IotSvgProperty = { + id: '', + name: '', + type: IotSvgPropertyType.text, + default: '' + }; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const propertyControl = this.fb.control(property, []); + propertiesArray.push(propertyControl); + setTimeout(() => { + const propertyRow = this.propertyRows.get(this.propertyRows.length-1); + propertyRow.onAdd(() => { + this.removeProperty(propertiesArray.length-1); + }); + }); + } + + private preparePropertiesFormArray(properties: IotSvgProperty[] | undefined): UntypedFormArray { + const propertiesControls: Array = []; + if (properties) { + properties.forEach((property) => { + propertiesControls.push(this.fb.control(property, [])); + }); + } + return this.fb.array(propertiesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html new file mode 100644 index 0000000000..8a76578487 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html @@ -0,0 +1,184 @@ + +
+
{{ panelTitle | translate }}
+
+
+
scada.property.id
+ + + +
+
+
scada.property.name
+ + + +
+
+
scada.property.type
+ + + + {{ iotSvgPropertyTypeTranslations.get(type) | translate }} + + + +
+
+
scada.property.default-value
+ + + + + + + + + + + + + + +
+
+
scada.property.number-settings
+
+
scada.property.min
+ + + + +
scada.property.max
+ + + + +
scada.property.step
+ + + +
+
+
+ + {{ 'scada.property.value-required' | translate }} + +
+
+ + + + {{ 'scada.property.advanced-ui-settings' | translate }} + + + +
+
scada.property.sub-label
+ + + +
+
+ + {{ 'scada.property.vertical-divider-after' | translate }} + +
+
+
scada.property.input-field-suffix
+ + + +
+
+
scada.property.disable-on-property
+ + + + {{ prop }} + + + +
+
+
scada.property.property-row-classes
+ + + + {{ clazz }} + + + +
+
+
scada.property.property-field-classes
+ + + + {{ clazz }} + + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss new file mode 100644 index 0000000000..c4b5b153d9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright ยฉ 2016-2024 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'; + +.tb-scada-symbol-property-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-property-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-scada-symbol-property-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-property-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts new file mode 100644 index 0000000000..9be8959dca --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts @@ -0,0 +1,129 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { + IotSvgProperty, iotSvgPropertyFieldClasses, iotSvgPropertyRowClasses, + IotSvgPropertyType, + iotSvgPropertyTypes, + iotSvgPropertyTypeTranslations +} from '@home/components/widget/lib/svg/iot-svg.models'; +import { WidgetService } from '@core/http/widget.service'; +import { defaultPropertyValue } from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { ValueType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-scada-symbol-property-panel', + templateUrl: './scada-symbol-property-panel.component.html', + styleUrls: ['./scada-symbol-property-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertyPanelComponent implements OnInit { + + ValueType = ValueType; + + IotSvgPropertyType = IotSvgPropertyType; + + iotSvgPropertyTypes = iotSvgPropertyTypes; + iotSvgPropertyTypeTranslations = iotSvgPropertyTypeTranslations; + + iotSvgPropertyRowClasses = iotSvgPropertyRowClasses; + + iotSvgPropertyFieldClasses = iotSvgPropertyFieldClasses; + + @Input() + isAdd = false; + + @Input() + property: IotSvgProperty; + + @Input() + booleanPropertyIds: string[]; + + @Input() + popover: TbPopoverComponent; + + @Output() + propertySettingsApplied = new EventEmitter(); + + panelTitle: string; + + propertyFormGroup: UntypedFormGroup; + + private propertyType: IotSvgPropertyType; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService) { + } + + ngOnInit(): void { + this.panelTitle = this.isAdd ? 'scada.property.add-property' : 'scada.property.property-settings'; + this.propertyType = this.property.type; + this.propertyFormGroup = this.fb.group( + { + id: [this.property.id, [Validators.required]], + name: [this.property.name, [Validators.required]], + type: [this.property.type, [Validators.required]], + default: [this.property.default, []], + required: [this.property.required, []], + subLabel: [this.property.subLabel, []], + divider: [this.property.divider, []], + fieldSuffix: [this.property.fieldSuffix, []], + disableOnProperty: [this.property.disableOnProperty, []], + rowClass: [(this.property.rowClass || '').split(' '), []], + fieldClass: [(this.property.fieldClass || '').split(' '), []], + min: [this.property.min, []], + max: [this.property.max, []], + step: [this.property.step, [Validators.min(0)]] + } + ); + this.propertyFormGroup.get('type').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyPropertySettings() { + const property = this.propertyFormGroup.getRawValue(); + property.rowClass = (property.rowClass || []).join(' '); + property.fieldClass = (property.fieldClass || []).join(' '); + this.propertySettingsApplied.emit(property); + } + + private updateValidators() { + const type: IotSvgPropertyType = this.propertyFormGroup.get('type').value; + if (type === IotSvgPropertyType.number) { + this.propertyFormGroup.get('min').enable({emitEvent: false}); + this.propertyFormGroup.get('max').enable({emitEvent: false}); + this.propertyFormGroup.get('step').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('min').disable({emitEvent: false}); + this.propertyFormGroup.get('max').disable({emitEvent: false}); + this.propertyFormGroup.get('step').disable({emitEvent: false}); + } + if (this.propertyType !== type) { + const defaultValue = defaultPropertyValue(type); + this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); + this.propertyType = type; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html new file mode 100644 index 0000000000..7016461cce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html @@ -0,0 +1,49 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss new file mode 100644 index 0000000000..5c0b4f0166 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright ยฉ 2016-2024 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. + */ +.tb-scada-symbol-metadata-property-row { + .tb-id-field { + flex: 1 1 40%; + } + .tb-name-field { + flex: 1 1 40%; + } + .tb-type-field { + flex: 1 1 20%; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts new file mode 100644 index 0000000000..60a56aae50 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts @@ -0,0 +1,280 @@ +/// +/// Copyright ยฉ 2016-2024 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 { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + IotSvgProperty, + IotSvgPropertyType, + iotSvgPropertyTypes, + iotSvgPropertyTypeTranslations +} from '@home/components/widget/lib/svg/iot-svg.models'; +import { deepClone } from '@core/utils'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { constantColor, Font } from '@shared/models/widget-settings.models'; +import { + ScadaSymbolPropertyPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component'; +import { + ScadaSymbolPropertiesComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component'; + +export const propertyValid = (property: IotSvgProperty): boolean => !(!property.id || !property.name || !property.type); + +export const defaultPropertyValue = (type: IotSvgPropertyType): any => { + switch (type) { + case IotSvgPropertyType.text: + return ''; + case IotSvgPropertyType.number: + return 0; + case IotSvgPropertyType.switch: + return false; + case IotSvgPropertyType.color: + return '#000'; + case IotSvgPropertyType.color_settings: + return constantColor('#000'); + case IotSvgPropertyType.font: + return { + size: 12, + sizeUnit: 'px', + family: 'Roboto', + weight: 'normal', + style: 'normal', + lineHeight: '1' + } as Font; + case IotSvgPropertyType.units: + return ''; + } +}; + +@Component({ + selector: 'tb-scada-symbol-metadata-property-row', + templateUrl: './scada-symbol-property-row.component.html', + styleUrls: ['./scada-symbol-property-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, OnInit, Validator { + + @ViewChild('idInput') + idInput: ElementRef; + + @ViewChild('editButton') + editButton: MatButton; + + iotSvgPropertyTypes = iotSvgPropertyTypes; + iotSvgPropertyTypeTranslations = iotSvgPropertyTypeTranslations; + + @Input() + disabled: boolean; + + @Input() + index: number; + + @Input() + booleanPropertyIds: string[]; + + @Output() + propertyRemoved = new EventEmitter(); + + propertyRowFormGroup: UntypedFormGroup; + + modelValue: IotSvgProperty; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private propertiesComponent: ScadaSymbolPropertiesComponent) { + } + + ngOnInit() { + this.propertyRowFormGroup = this.fb.group({ + id: [null, [this.propertyIdValidator()]], + name: [null, [Validators.required]], + type: [null, [Validators.required]] + }); + this.propertyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.propertyRowFormGroup.get('type').valueChanges.subscribe((newType: IotSvgPropertyType) => { + this.onTypeChanged(newType); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.propertyRowFormGroup.disable({emitEvent: false}); + } else { + this.propertyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: IotSvgProperty): void { + this.modelValue = value; + this.propertyRowFormGroup.patchValue( + { + id: value?.id, + name: value?.name, + type: value?.type + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + editProperty($event: Event, matButton: MatButton, add = false, editCanceled = () => {}) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + isAdd: add, + booleanPropertyIds: this.booleanPropertyIds, + property: deepClone(this.modelValue) + }; + const scadaSymbolPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ScadaSymbolPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + scadaSymbolPropertyPanelPopover.tbComponentRef.instance.popover = scadaSymbolPropertyPanelPopover; + scadaSymbolPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => { + scadaSymbolPropertyPanelPopover.hide(); + this.propertyRowFormGroup.patchValue( + { + id: property.id, + name: property.name, + type: property.type + }, {emitEvent: false} + ); + this.modelValue = property; + this.propagateChange(this.modelValue); + }); + scadaSymbolPropertyPanelPopover.tbDestroy.subscribe(() => { + if (!propertyValid(this.modelValue)) { + editCanceled(); + } + }); + } + } + + focus() { + this.idInput.nativeElement.scrollIntoView(); + this.idInput.nativeElement.focus(); + } + + onAdd(onCanceled: () => void) { + this.idInput.nativeElement.scrollIntoView(); + this.editProperty(null, this.editButton, true, onCanceled); + } + + public validate(c: UntypedFormControl) { + const idControl = this.propertyRowFormGroup.get('id'); + if (idControl.hasError('propertyIdNotUnique')) { + idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); + } + if (idControl.hasError('propertyIdNotUnique')) { + this.propertyRowFormGroup.get('id').markAsTouched(); + return { + propertyIdNotUnique: true + }; + } + const property: IotSvgProperty = {...this.modelValue, ...this.propertyRowFormGroup.value}; + if (!propertyValid(property)) { + return { + property: true + }; + } + return null; + } + + private propertyIdValidator(): ValidatorFn { + return control => { + if (!control.value) { + return { + required: true + }; + } + if (!this.propertiesComponent.propertyIdUnique(control.value, this.index)) { + return { + propertyIdNotUnique: true + }; + } + return null; + }; + } + + private onTypeChanged(newType: IotSvgPropertyType) { + this.modelValue = {...this.modelValue, ...{type: newType}}; + this.modelValue.default = defaultPropertyValue(newType); + } + + private updateModel() { + const value: IotSvgProperty = this.propertyRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index c595f63071..086ca454d7 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -320,6 +320,9 @@ export class ScadaSymbolComponent extends PageComponent } private reset(): void { + if (this.symbolMetadata) { + this.symbolMetadata.selectedOption = 'general'; + } this.previewMode = false; } 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 42c5a644c2..1d49829f21 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3439,7 +3439,8 @@ "true-label": "True label", "false-label": "False label", "state-label": "State label", - "default-payload": "Default payload" + "default-payload": "Default payload", + "not-unique-behavior-ids-error": "Behavior Ids must be unique!" }, "property": { "property": "Property", @@ -3456,7 +3457,21 @@ "no-properties": "No properties configured", "add-property": "Add property", "property-settings": "Property settings", - "remove-property": "Remove property" + "remove-property": "Remove property", + "default-value": "Default value", + "value-required": "Value required", + "number-settings": "Number settings", + "min": "Min", + "max": "Max", + "step": "Step", + "advanced-ui-settings": "Advanced UI settings", + "disable-on-property": "Disable on property", + "sub-label": "Sub label", + "vertical-divider-after": "Vertical divider after", + "input-field-suffix": "Input field suffix", + "property-row-classes": "Property row classes", + "property-field-classes": "Property field classes", + "not-unique-property-ids-error": "Property Ids must be unique!" } }, "item": { From a4504ad9ce79e7c1897454471beef5793909b264 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 28 May 2024 16:49:18 +0300 Subject: [PATCH 027/331] UI: SCADA symbol functions completers and highlights. --- .../widget/lib/svg/iot-svg.models.ts | 18 +- .../scada-symbol-metadata-tag.component.html | 10 +- .../scada-symbol-metadata-tag.component.ts | 19 ++ .../scada-symbol-metadata-tags.component.html | 2 + .../scada-symbol-metadata-tags.component.ts | 7 + .../scada-symbol-metadata.component.html | 7 +- .../scada-symbol-metadata.component.scss | 36 +++ .../scada-symbol-metadata.component.ts | 72 ++++- .../pages/scada-symbol/scada-symbol.models.ts | 253 +++++++++++++++++- .../shared/components/js-func.component.ts | 34 ++- .../src/app/shared/models/ace/ace.models.ts | 6 + .../shared/models/ace/completion.models.ts | 4 + 12 files changed, 449 insertions(+), 19 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index c77065fbf8..61fdcb9b58 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -61,13 +61,13 @@ export interface IotSvgContext { properties: {[id: string]: any}; } -export type IotSvgStateRenderFunction = (svg: Svg, ctx: IotSvgContext) => void; +export type IotSvgStateRenderFunction = (ctx: IotSvgContext, svg: Svg) => void; -export type IotSvgTagStateRenderFunction = (element: Element, ctx: IotSvgContext) => void; +export type IotSvgTagStateRenderFunction = (ctx: IotSvgContext, element: Element) => void; export type IotSvgActionTrigger = 'click'; -export type IotSvgActionFunction = (event: Event, ctx: IotSvgContext) => void; +export type IotSvgActionFunction = (ctx: IotSvgContext, event: Event) => void; export interface IotSvgAction { actionFunction?: string; action?: IotSvgActionFunction; @@ -421,13 +421,13 @@ export class IotSvgObject { } private prepareMetadata() { - this.metadata.stateRender = parseFunction(this.metadata.stateRenderFunction, ['svg', 'ctx']) || (() => {}); + this.metadata.stateRender = parseFunction(this.metadata.stateRenderFunction, ['ctx', 'svg']) || (() => {}); for (const tag of this.metadata.tags) { - tag.stateRender = parseFunction(tag.stateRenderFunction, ['element', 'ctx']) || (() => {}); + tag.stateRender = parseFunction(tag.stateRenderFunction, ['ctx', 'element']) || (() => {}); if (tag.actions) { for (const trigger of Object.keys(tag.actions)) { const action = tag.actions[trigger]; - action.action = parseFunction(action.actionFunction, ['event', 'ctx']) || (() => {}); + action.action = parseFunction(action.actionFunction, ['ctx', 'event']) || (() => {}); } } } @@ -483,7 +483,7 @@ export class IotSvgObject { elements.forEach(element => { element.attr('cursor', 'pointer'); element.on(trigger, (event) => { - action.action(event, this.context); + action.action(this.context, event); }); }); } @@ -637,11 +637,11 @@ export class IotSvgObject { } private renderState(): void { - this.metadata.stateRender(this.svgShape, this.context); + this.metadata.stateRender(this.context, this.svgShape); for (const tag of this.metadata.tags) { const elements = this.context.tags[tag.tag];// this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); elements.forEach(element => { - tag.stateRender(element, this.context); + tag.stateRender(this.context, element); }); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html index 01914d1e32..88302152c5 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -49,7 +49,10 @@ @@ -65,7 +68,10 @@ diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index 663a87d8fa..7b5e36266b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -38,6 +38,13 @@ import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models'; import { MatExpansionPanel } from '@angular/material/expansion'; import { JsFuncComponent } from '@shared/components/js-func.component'; import { MatSelect } from '@angular/material/select'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { + scadaSymbolClickActionHighlightRules, + scadaSymbolClickActionPropertiesHighlightRules, + scadaSymbolElementStateRenderHighlightRules, + scadaSymbolElementStateRenderPropertiesHighlightRules +} from '@home/pages/scada-symbol/scada-symbol.models'; @Component({ selector: 'tb-scada-symbol-metadata-tag', @@ -78,6 +85,12 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On @Input() tags: string[]; + @Input() + elementStateRenderFunctionCompleter: TbEditorCompleter; + + @Input() + clickActionFunctionCompleter: TbEditorCompleter; + @Output() tagRemoved = new EventEmitter(); @@ -87,6 +100,12 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On modelValue: IotSvgTag; + scadaSymbolElementStateRenderHighlightRules = scadaSymbolElementStateRenderHighlightRules; + scadaSymbolElementStateRenderPropertiesHighlightRules = scadaSymbolElementStateRenderPropertiesHighlightRules; + + scadaSymbolClickActionHighlightRules = scadaSymbolClickActionHighlightRules; + scadaSymbolClickActionPropertiesHighlightRules = scadaSymbolClickActionPropertiesHighlightRules; + private propagateChange = (_val: any) => {}; constructor(private fb: UntypedFormBuilder, diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html index 5e3b49a4aa..6ce57301af 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -19,6 +19,8 @@
- - gateway.statistics.check-connectors-configuration - - - {{ 'gateway.statistics.check-connectors-configuration-required' | translate }} - - - {{ 'gateway.statistics.check-connectors-configuration-min' | translate }} - - - {{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }} - - +
+ + gateway.statistics.check-connectors-configuration + + + {{ 'gateway.statistics.check-connectors-configuration-required' | translate }} + + + {{ 'gateway.statistics.check-connectors-configuration-min' | translate }} + + + {{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }} + + + + gateway.statistics.max-payload-size-bytes + + + {{ 'gateway.statistics.max-payload-size-bytes-required' | translate }} + + + {{ 'gateway.statistics.max-payload-size-bytes-min' | translate }} + + + {{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }} + + +
+
+ + gateway.statistics.min-pack-size-to-send + + + {{ 'gateway.statistics.min-pack-size-to-send-required' | translate }} + + + {{ 'gateway.statistics.min-pack-size-to-send-min' | translate }} + + + {{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }} + + +
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 9f92072680..6c2d657fae 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3219,9 +3219,17 @@ "send-period-min": "Statistic send period can not be less then 60", "send-period-pattern": "Statistic send period is not valid", "check-connectors-configuration": "Check connectors configuration (in sec)", + "max-payload-size-bytes": "Max payload size in bytes", + "min-pack-size-to-send": "Min packet size to send", "check-connectors-configuration-required": "Check connectors configuration is required", + "max-payload-size-bytes-required": "Max payload size in bytes is required", + "min-pack-size-to-send-required": "Min packet size to send is required", "check-connectors-configuration-min": "Check connectors configuration can not be less then 1", + "max-payload-size-bytes-min": "Max payload size in bytes can not be less then 1", + "min-pack-size-to-send-min": "Min packet size to send can not be less then 1", "check-connectors-configuration-pattern": "Check connectors configuration is not valid", + "max-payload-size-bytes-pattern": "Max payload size in bytes is not valid", + "min-pack-size-to-send-pattern": "Min packet size to send is not valid", "add": "Add command", "timeout": "Timeout", "timeout-ms": "Timeout (in ms)", From 83c12d3b4b046217fd0d2c1230b9186068c4093a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 15 Aug 2024 19:10:29 +0300 Subject: [PATCH 211/331] added tb-dialog class --- ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts b/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts index 538839347b..f39716dc5e 100644 --- a/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts @@ -91,7 +91,7 @@ export class EntityConflictInterceptor implements HttpInterceptor { const dialogRef = this.dialog.open(EntityConflictDialogComponent, { disableClose: true, data: { message, entity }, - panelClass: ['tb-fullscreen-dialog'], + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], }); return dialogRef.afterClosed(); From b89951bdb315831702747e00ccb4805b35595dc7 Mon Sep 17 00:00:00 2001 From: Max Petrov <93397261+maxunbearable@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:35:55 +0300 Subject: [PATCH 212/331] translate order change Co-authored-by: Vladyslav Prykhodko --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 6c2d657fae..6237f84e90 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3220,16 +3220,16 @@ "send-period-pattern": "Statistic send period is not valid", "check-connectors-configuration": "Check connectors configuration (in sec)", "max-payload-size-bytes": "Max payload size in bytes", - "min-pack-size-to-send": "Min packet size to send", - "check-connectors-configuration-required": "Check connectors configuration is required", "max-payload-size-bytes-required": "Max payload size in bytes is required", - "min-pack-size-to-send-required": "Min packet size to send is required", - "check-connectors-configuration-min": "Check connectors configuration can not be less then 1", "max-payload-size-bytes-min": "Max payload size in bytes can not be less then 1", - "min-pack-size-to-send-min": "Min packet size to send can not be less then 1", - "check-connectors-configuration-pattern": "Check connectors configuration is not valid", "max-payload-size-bytes-pattern": "Max payload size in bytes is not valid", + "min-pack-size-to-send": "Min packet size to send", + "min-pack-size-to-send-required": "Min packet size to send is required", + "min-pack-size-to-send-min": "Min packet size to send can not be less then 1", "min-pack-size-to-send-pattern": "Min packet size to send is not valid", + "check-connectors-configuration-required": "Check connectors configuration is required", + "check-connectors-configuration-min": "Check connectors configuration can not be less then 1", + "check-connectors-configuration-pattern": "Check connectors configuration is not valid", "add": "Add command", "timeout": "Timeout", "timeout-ms": "Timeout (in ms)", From 840751b3caec5a574340ee040b25690f45ede5d2 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 16 Aug 2024 12:17:57 +0300 Subject: [PATCH 213/331] hints and opc-ua adjustments --- .../opc-server-config.component.html | 33 +++++++++++++++---- .../opc-server-config.component.ts | 9 +++-- .../gateway-configuration.component.html | 10 ++++++ .../gateway-configuration.component.ts | 6 ++-- .../lib/gateway/gateway-widget.models.ts | 3 +- .../assets/locale/locale.constant-en_US.json | 19 +++++++---- .../opcua_asyncio.json | 5 +-- 7 files changed, 63 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html index 8c3f3aaf4b..057a4d1882 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html @@ -69,15 +69,36 @@
- + + warning + + +
+ +
+
+
{{ 'gateway.poll-period' | translate }}
+
+
+ + + warning diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 5b14f16db2..46e7fd0c9c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -80,7 +80,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator name: ['', []], url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]], - scanPeriodInMillis: [1000, [Validators.required, Validators.min(1000)]], + scanPeriodInSec: [3600, [Validators.required, Validators.min(1)]], + pollPeriodInMillis: [5000, [Validators.required, Validators.min(50)]], enableSubscriptions: [true, []], subCheckPeriodInMillis: [100, [Validators.required, Validators.min(100)]], showMap: [false, []], @@ -118,7 +119,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator writeValue(serverConfig: ServerConfig): void { const { timeoutInMillis = 1000, - scanPeriodInMillis = 1000, + scanPeriodInSec = 3600, + pollPeriodInMillis = 5000, enableSubscriptions = true, subCheckPeriodInMillis = 100, showMap = false, @@ -129,7 +131,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator this.serverConfigFormGroup.reset({ ...serverConfig, timeoutInMillis, - scanPeriodInMillis, + scanPeriodInSec, + pollPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html index 4aa806a6b9..36b26cc7a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html @@ -726,6 +726,10 @@ {{ 'gateway.min-pack-send-delay-min' | translate }} + + {{ 'gateway.min-pack-send-delay-min-pattern' | translate }} + info_outlined @@ -779,6 +783,9 @@ *ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')"> {{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }} + info_outlined +
@@ -797,6 +804,9 @@ *ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')"> {{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }} + info_outlined +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts index 3b2e0e9fc6..55121da8c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts @@ -102,9 +102,9 @@ export class GatewayConfigurationComponent implements OnInit { statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]], commands: this.fb.array([], []) }), - maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], - minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], - minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]], + minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]], + minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]], handleDeviceRenaming: [true, []], checkingDeviceActivity: this.fb.group({ checkDeviceInactivity: [false, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 8ee57e068f..0ad4975a4b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -150,7 +150,8 @@ export interface ServerConfig { name: string; url: string; timeoutInMillis: number; - scanPeriodInMillis: number; + scanPeriodInSec: number; + pollPeriodInMillis: number; enableSubscriptions: boolean; subCheckPeriodInMillis: number; showMap: boolean; 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 6237f84e90..ba4f031d14 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3016,7 +3016,8 @@ "method-required": "Method name is required.", "min-pack-send-delay": "Min pack send delay (in ms)", "min-pack-send-delay-required": "Min pack send delay is required", - "min-pack-send-delay-min": "Min pack send delay can not be less then 0", + "min-pack-send-delay-min": "Min pack send delay can not be less then 10", + "min-pack-send-delay-pattern": "Min pack send delay is not valid", "mode": "Mode", "model-name": "Model name", "mqtt-version": "MQTT version", @@ -3040,6 +3041,7 @@ "password-required": "Password is required.", "permit-without-calls": "Keep alive permit without calls", "poll-period": "Poll period (ms)", + "poll-period-error": "Poll period should be at least {{min}} (ms).", "port": "Port", "port-required": "Port is required.", "port-limits-error": "Port should be number from {{min}} to {{max}}.", @@ -3176,8 +3178,8 @@ "without-response": "Without response", "other": "Other", "save-tip": "Save configuration file", - "scan-period": "Scan period (ms)", - "scan-period-error": "Scan period should be at least {{min}} (ms).", + "scan-period": "Scan period (s)", + "scan-period-error": "Scan period should be at least {{min}} (s).", "sub-check-period": "Subscription check period (ms)", "sub-check-period-error": "Subscription check period should be at least {{min}} (ms).", "security": "Security", @@ -3221,11 +3223,11 @@ "check-connectors-configuration": "Check connectors configuration (in sec)", "max-payload-size-bytes": "Max payload size in bytes", "max-payload-size-bytes-required": "Max payload size in bytes is required", - "max-payload-size-bytes-min": "Max payload size in bytes can not be less then 1", + "max-payload-size-bytes-min": "Max payload size in bytes can not be less then 100", "max-payload-size-bytes-pattern": "Max payload size in bytes is not valid", "min-pack-size-to-send": "Min packet size to send", "min-pack-size-to-send-required": "Min packet size to send is required", - "min-pack-size-to-send-min": "Min packet size to send can not be less then 1", + "min-pack-size-to-send-min": "Min packet size to send can not be less then 100", "min-pack-size-to-send-pattern": "Min packet size to send is not valid", "check-connectors-configuration-required": "Check connectors configuration is required", "check-connectors-configuration-min": "Check connectors configuration can not be less then 1", @@ -3387,12 +3389,15 @@ "file": "Your data will be stored in separated files and will be saved even after the gateway restart.", "sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.", "opc-timeout": "Timeout in milliseconds for connecting to OPC-UA server.", - "scan-period": "Period in milliseconds to rescan the server.", + "scan-period": "Period in seconds to rescan the server.", "sub-check-period": "Period to check the subscriptions in the OPC-UA server.", - "enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.", + "enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInSec.", "show-map": "Show nodes on scanning.", "method-name": "Name of method on OPC-UA server.", "arguments": "Arguments for the method (will be overwritten by arguments from the RPC request).", + "min-pack-size-to-send": "Minimum package size for sending.", + "max-payload-size-bytes": "Maximum package size in bytes", + "poll-period": "Period in milliseconds to read data from nodes.", "modbus": { "framer-type": "Type of a framer (Socket, RTU, or ASCII), if needed.", "host": "Hostname or IP address of Modbus server.", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json index 91ee295648..453f23735d 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json @@ -3,7 +3,8 @@ "name": "OPC-UA Default Server", "url": "localhost:4840/freeopcua/server/", "timeoutInMillis": 5000, - "scanPeriodInMillis": 5000, + "scanPeriodInSec": 3600, + "pollPeriodInMillis": 5000, "disableSubscriptions": false, "subCheckPeriodInMillis": 100, "showMap": false, @@ -49,4 +50,4 @@ } ] } -} \ No newline at end of file +} From 9d1f162fa5a01c06b9357e91e80e02af4804759f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 16 Aug 2024 13:20:38 +0300 Subject: [PATCH 214/331] SNMP v3: fix unsupported authentication and protocol versions --- .../transport/snmp/SnmpTransportContext.java | 4 ++-- .../snmp/service/SnmpAuthService.java | 19 +++++++++++++------ .../snmp/service/SnmpTransportService.java | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java index 6e30b06a2c..ffd2b7e33c 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java @@ -127,7 +127,7 @@ public class SnmpTransportContext extends TransportContext { .build(); registerSessionMsgListener(sessionContext); } catch (Exception e) { - log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.toString()); + log.error("Failed to establish session for SNMP device {}", device.getId(), e); transportService.errorEvent(device.getTenantId(), device.getId(), "sessionEstablishing", e); return; } @@ -166,7 +166,7 @@ public class SnmpTransportContext extends TransportContext { log.trace("Configuration of the device {} was not updated", device); } } catch (Exception e) { - log.error("Failed to update session for SNMP device {}: {}", sessionContext.getDeviceId(), e.getMessage()); + log.error("Failed to update session for SNMP device {}", sessionContext.getDeviceId(), e); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, false, e); destroyDeviceSession(sessionContext); } diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java index b5f9cac09b..aca736c3ed 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java @@ -72,11 +72,18 @@ public class SnmpAuthService { OctetString engineId = new OctetString(deviceTransportConfig.getEngineId()); OID authenticationProtocol = new OID(deviceTransportConfig.getAuthenticationProtocol().getOid()); + byte[] authenticationPassphrase = SecurityProtocols.getInstance().passwordToKey(authenticationProtocol, + new OctetString(deviceTransportConfig.getAuthenticationPassphrase()), engineId.getValue()); + if (authenticationPassphrase == null) { + throw new UnsupportedOperationException("Authentication protocol " + deviceTransportConfig.getAuthenticationProtocol() + " is not supported"); + } + OID privacyProtocol = new OID(deviceTransportConfig.getPrivacyProtocol().getOid()); - OctetString authenticationPassphrase = new OctetString(deviceTransportConfig.getAuthenticationPassphrase()); - authenticationPassphrase = new OctetString(SecurityProtocols.getInstance().passwordToKey(authenticationProtocol, authenticationPassphrase, engineId.getValue())); - OctetString privacyPassphrase = new OctetString(deviceTransportConfig.getPrivacyPassphrase()); - privacyPassphrase = new OctetString(SecurityProtocols.getInstance().passwordToKey(privacyProtocol, authenticationProtocol, privacyPassphrase, engineId.getValue())); + byte[] privacyPassphrase = SecurityProtocols.getInstance().passwordToKey(privacyProtocol, + authenticationProtocol, new OctetString(deviceTransportConfig.getPrivacyPassphrase()), engineId.getValue()); + if (privacyPassphrase == null) { + throw new UnsupportedOperationException("Privacy protocol " + deviceTransportConfig.getPrivacyProtocol() + " is not supported"); + } USM usm = snmpTransportService.getSnmp().getUSM(); if (usm.hasUser(engineId, securityName)) { @@ -84,8 +91,8 @@ public class SnmpAuthService { } usm.addLocalizedUser( engineId.getValue(), username, - authenticationProtocol, authenticationPassphrase.getValue(), - privacyProtocol, privacyPassphrase.getValue() + authenticationProtocol, authenticationPassphrase, + privacyProtocol, privacyPassphrase ); UserTarget userTarget = new UserTarget(); diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java index 3a1d440d20..26008e2c82 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java @@ -148,6 +148,7 @@ public class SnmpTransportService implements TbTransportService, CommandResponde snmp.addNotificationListener(transportMapping, transportMapping.getListenAddress(), this); snmp.listen(); + SecurityProtocols.getInstance().addPredefinedProtocolSet(SecurityProtocols.SecurityProtocolSet.maxCompatibility); USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0); SecurityModels.getInstance().addSecurityModel(usm); } From 7cb15a0e00fff83fb0f8f8482f91e81644efd9a0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 16 Aug 2024 13:42:55 +0300 Subject: [PATCH 215/331] UI: Add manage dashboard layout and improved selector breakpoint --- .../core/services/dashboard-utils.service.ts | 73 +++++++-- .../add-widget-dialog.component.html | 3 +- .../add-widget-dialog.component.ts | 8 +- .../dashboard-page.component.html | 54 ++++--- .../dashboard-page.component.scss | 21 ++- .../dashboard-page.component.ts | 141 ++++++++++------- .../dashboard-page/dashboard-page.models.ts | 5 +- .../dashboard-settings-dialog.component.html | 43 +++-- .../dashboard-settings-dialog.component.ts | 148 +++++++++++++----- .../dashboard-toolbar.component.scss | 13 +- .../dashboard-page/edit-widget.component.html | 3 +- .../dashboard-page/edit-widget.component.ts | 6 +- .../add-new-breakpoint-dialog.component.html | 14 +- .../add-new-breakpoint-dialog.component.ts | 25 ++- .../layout/dashboard-layout.component.html | 4 +- .../layout/dashboard-layout.component.ts | 52 +++--- ...ge-dashboard-layouts-dialog.component.html | 128 +++++++-------- ...ge-dashboard-layouts-dialog.component.scss | 3 +- ...nage-dashboard-layouts-dialog.component.ts | 81 +++++----- ...select-dashboard-breakpoint.component.html | 5 +- .../select-dashboard-breakpoint.component.ts | 21 ++- .../dashboard/dashboard.component.ts | 17 +- .../mapping-table/mapping-table.component.ts | 3 +- .../modbus-data-keys-panel.component.ts | 2 - .../modbus-security-config.component.ts | 2 - .../modbus-slave-config.component.ts | 2 - .../modbus-slave-dialog.component.ts | 2 - .../opc-server-config.component.ts | 2 - .../workers-config-control.component.ts | 2 - .../widget/widget-components.module.ts | 4 +- .../widget/widget-config.component.html | 4 +- .../widget/widget-config.component.ts | 16 +- .../truncate-with-tooltip.directive.ts | 1 - .../src/app/shared/models/dashboard.models.ts | 46 +++++- ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-ar_AE.json | 1 - .../assets/locale/locale.constant-ca_ES.json | 1 - .../assets/locale/locale.constant-cs_CZ.json | 1 - .../assets/locale/locale.constant-en_US.json | 23 ++- .../assets/locale/locale.constant-es_ES.json | 1 - .../assets/locale/locale.constant-fr_FR.json | 1 - .../assets/locale/locale.constant-lt_LT.json | 1 - .../assets/locale/locale.constant-nl_BE.json | 1 - .../assets/locale/locale.constant-pl_PL.json | 1 - .../assets/locale/locale.constant-tr_TR.json | 1 - .../assets/locale/locale.constant-zh_CN.json | 1 - .../assets/locale/locale.constant-zh_TW.json | 1 - 47 files changed, 644 insertions(+), 347 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 51f01c7a1b..d85ae83880 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -18,8 +18,11 @@ import { Injectable } from '@angular/core'; import { UtilsService } from '@core/services/utils.service'; import { TimeService } from '@core/services/time.service'; import { + BreakpointId, breakpointIdIconMap, + breakpointIdTranslationMap, BreakpointInfo, BreakpointLayoutInfo, + BreakpointSystemId, Dashboard, DashboardConfiguration, DashboardLayout, @@ -41,7 +44,8 @@ import { TargetDeviceType, Widget, WidgetConfig, - WidgetConfigMode, WidgetSize, + WidgetConfigMode, + WidgetSize, widgetType, WidgetTypeDescriptor } from '@app/shared/models/widget.models'; @@ -53,16 +57,18 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; import { MediaBreakpoints } from '@shared/models/constants'; +import { TranslateService } from '@ngx-translate/core'; @Injectable({ providedIn: 'root' }) export class DashboardUtilsService { - private systemBreakpoints: BreakpointInfo[]; + private systemBreakpoints: {[key in BreakpointSystemId]?: BreakpointInfo}; constructor(private utils: UtilsService, - private timeService: TimeService) { + private timeService: TimeService, + private translate: TranslateService) { } public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard { @@ -1001,13 +1007,18 @@ export class DashboardUtilsService { } private loadSystemBreakpoints() { - this.systemBreakpoints=[{id: 'default'}]; - const dashboardMediaBreakpointIds = ['xs', 'sm', 'md', 'lg', 'xl']; + this.systemBreakpoints = {}; + const dashboardMediaBreakpointIds: BreakpointSystemId[] = ['xs', 'sm', 'md', 'lg', 'xl']; dashboardMediaBreakpointIds.forEach(breakpoint => { const value = MediaBreakpoints[breakpoint]; - const minWidth = value.match(/min-width:\s*(\d+)px/); - const maxWidth = value.match(/max-width:\s*(\d+)px/); - this.systemBreakpoints.push({id: breakpoint, minWidth: minWidth?.[1], maxWidth: maxWidth?.[1]}); + const minWidth = value.match(/min-width:\s*(\d+)px/)?.[1]; + const maxWidth = value.match(/max-width:\s*(\d+)px/)?.[1]; + this.systemBreakpoints[breakpoint] = ({ + id: breakpoint, + minWidth: minWidth ? Number(minWidth) : undefined, + maxWidth: maxWidth ? Number(maxWidth) : undefined, + value + }); }); } @@ -1015,6 +1026,50 @@ export class DashboardUtilsService { if(!this.systemBreakpoints) { this.loadSystemBreakpoints(); } - return this.systemBreakpoints; + const breakpointsList = Object.values(this.systemBreakpoints); + breakpointsList.unshift({id: 'default'}); + return breakpointsList; + } + + getBreakpoints(): string[] { + if(!this.systemBreakpoints) { + this.loadSystemBreakpoints(); + } + return Object.values(this.systemBreakpoints).map(item => item.value); + } + + getBreakpointInfoByValue(breakpointValue: string): BreakpointInfo { + if(!this.systemBreakpoints) { + this.loadSystemBreakpoints(); + } + return Object.values(this.systemBreakpoints).find(item => item.value === breakpointValue); + } + + getBreakpointInfoById(breakpointId: BreakpointId): BreakpointInfo { + if(!this.systemBreakpoints) { + this.loadSystemBreakpoints(); + } + return this.systemBreakpoints[breakpointId]; + } + + getBreakpointName(breakpointId: BreakpointId): string { + if (breakpointIdTranslationMap.has(breakpointId)) { + return this.translate.instant(breakpointIdTranslationMap.get(breakpointId)); + } + return breakpointId; + } + + getBreakpointIcon(breakpointId: BreakpointId): string { + if (breakpointIdIconMap.has(breakpointId)) { + return breakpointIdIconMap.get(breakpointId); + } + return 'desktop_windows'; + } + + getBreakpointSizeDescription(breakpointId: BreakpointId): string { + const currentData = this.getBreakpointInfoById(breakpointId); + const minStr = isDefined(currentData?.minWidth) ? `min ${currentData.minWidth}px` : ''; + const maxStr = isDefined(currentData?.maxWidth) ? `max ${currentData.maxWidth}px` : ''; + return minStr && maxStr ? `${minStr} - ${maxStr}` : `${minStr}${maxStr}`; } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 8baaa67ead..b86d41a5b5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -44,7 +44,8 @@ [dashboard]="dashboard" [widget]="widget" [hideHeader]="hideHeader" - [scada]="data.scada" + [showLayoutConfig]="showLayoutConfig" + [isDefaultBreakpoint]="isDefaultBreakpoint" isAdd formControlName="widgetConfig"> diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 662739295b..1d2fb7bfcf 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -37,7 +37,8 @@ export interface AddWidgetDialogData { stateController: IStateController; widget: Widget; widgetInfo: WidgetInfo; - scada: boolean; + showLayoutConfig: boolean; + isDefaultBreakpoint: boolean; } @Component({ @@ -60,6 +61,9 @@ export class AddWidgetDialogComponent extends DialogComponent +
- - + fxLayoutAlign="end center" fxLayoutGap.lt-lg="3px" fxLayoutGap.lg="6px" fxLayoutGap.gt-lg="12px"> + + +
@@ -91,7 +95,7 @@ (click)="toggleLayouts()"> {{isRightLayoutOpened ? 'arrow_back' : 'menu'}} - - -
-
-
dashboard.layout
- - - - {{ layoutTypeTranslations.get(type) | translate }} - - - -
-
-
-
-
-
layout.breakpoints
-
layout.size
-
-
-
- -
-
- {{ breakpoint.icon }} -
-
- {{ breakpoint.name }} -
-
- {{ breakpoint.descriptionSize }} -
-
- - +
+
+
+
dashboard.layout
+ + + + {{ layoutTypeTranslations.get(type) | translate }} + + + +
+
+
+
+
+
layout.breakpoints
+
layout.size
+
+
+
+ +
+
+ {{ breakpoint.icon }} +
+
+ {{ breakpoint.name }} +
+
+ {{ breakpoint.descriptionSize }} +
+
+ + +
-
- - + + +
+
- -
+
-
+
{{ 'widget-config.mobile-hide' | translate }}
-
+
{{ 'widget-config.desktop-hide' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 544a70e9fd..2bcc4b04e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -38,7 +38,7 @@ import { JsonSchema, JsonSettingsSchema, TargetDevice, - TargetDeviceType, targetDeviceValid, + targetDeviceValid, Widget, WidgetConfigMode, widgetType @@ -84,8 +84,8 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { coerceBoolean } from '@shared/decorators/coercion'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; -import Timeout = NodeJS.Timeout; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import Timeout = NodeJS.Timeout; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -152,7 +152,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe @Input() @coerceBoolean() - scada = false; + showLayoutConfig = true; + + @Input() + @coerceBoolean() + isDefaultBreakpoint = true; @Input() disabled: boolean; @@ -345,10 +349,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe value: 'actions' } ); - if (!this.scada) { + if (this.showLayoutConfig) { this.headerOptions.push( { - name: this.translate.instant('widget-config.mobile'), + name: this.isDefaultBreakpoint + ? this.translate.instant('widget-config.mobile') + : this.translate.instant('widget-config.list-option'), value: 'mobile' } ); diff --git a/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts index ac67f33e35..d37841be6f 100644 --- a/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts +++ b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts @@ -29,7 +29,6 @@ import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; import { coerceBoolean } from '@shared/decorators/coercion'; @Directive({ - standalone: true, selector: '[tbTruncateWithTooltip]', providers: [MatTooltip], }) diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 722ff50b47..04606fb612 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -18,7 +18,7 @@ import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { DashboardId } from '@shared/models/id/dashboard-id'; import { TenantId } from '@shared/models/id/tenant-id'; import { ShortCustomerInfo } from '@shared/models/customer.model'; -import { LegendPosition, Widget } from './widget.models'; +import { Widget } from './widget.models'; import { Timewindow } from '@shared/models/time/time.models'; import { EntityAliases } from './alias.models'; import { Filters } from '@shared/models/query/query.models'; @@ -65,6 +65,20 @@ export const layoutTypeTranslationMap = new Map( ] ); +export enum ViewFormatType { + grid = 'grid', + list = 'list', +} + +export const viewFormatTypes = Object.keys(ViewFormatType) as ViewFormatType[]; + +export const viewFormatTypeTranslationMap = new Map( + [ + [ ViewFormatType.grid, 'dashboard.view-format-type-grid' ], + [ ViewFormatType.list, 'dashboard.view-format-type-list' ], + ] +); + export interface GridSettings { layoutType?: LayoutType; backgroundColor?: string; @@ -72,9 +86,11 @@ export interface GridSettings { minColumns?: number; margin?: number; outerMargin?: boolean; + viewFormat?: ViewFormatType; backgroundSizeMode?: string; backgroundImageUrl?: string; autoFillHeight?: boolean; + rowHeight?: number; mobileAutoFillHeight?: boolean; mobileRowHeight?: number; mobileDisplayLayoutFirst?: boolean; @@ -85,7 +101,7 @@ export interface GridSettings { export interface DashboardLayout { widgets: WidgetLayouts; gridSettings: GridSettings; - breakpoints?: {[breakpointId: string]: Omit}; + breakpoints?: {[breakpointId in BreakpointId]?: Omit}; } export declare type DashboardLayoutInfo = {[breakpointId: string]: BreakpointLayoutInfo}; @@ -96,11 +112,33 @@ export interface BreakpointLayoutInfo { gridSettings?: GridSettings; } +export declare type BreakpointSystemId = 'default' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export declare type BreakpointId = BreakpointSystemId | string; + export interface BreakpointInfo { - id: string; + id: BreakpointId; maxWidth?: number; minWidth?: number; -} + value?: string; +} + +export const breakpointIdTranslationMap = new Map([ + ['default', 'dashboard.breakpoints-id.default'], + ['xs', 'dashboard.breakpoints-id.xs'], + ['sm', 'dashboard.breakpoints-id.sm'], + ['md', 'dashboard.breakpoints-id.md'], + ['lg', 'dashboard.breakpoints-id.lg'], + ['xl', 'dashboard.breakpoints-id.xl'], +]); + +export const breakpointIdIconMap = new Map([ + ['default', 'desktop_windows'], + ['xs', 'phone_iphone'], + ['sm', 'tablet_mac'], + ['md', 'computer'], + ['lg', 'monitor'], + ['xl', 'desktop_windows'], +]); export interface LayoutDimension { type?: LayoutDimensionType; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 4e925484ca..3d4edacd1d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -67,6 +67,7 @@ import { ColorPickerModule } from '@iplab/ngx-color-picker'; import { NgxHmCarouselModule } from 'ngx-hm-carousel'; import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; import { UserMenuComponent } from '@shared/components/user-menu.component'; +import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive'; import { NospacePipe } from '@shared/pipe/nospace.pipe'; import { TranslateModule } from '@ngx-translate/core'; import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; @@ -356,6 +357,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) NavTreeComponent, LedLightComponent, MarkdownEditorComponent, + TruncateWithTooltipDirective, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -611,6 +613,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) NavTreeComponent, LedLightComponent, MarkdownEditorComponent, + TruncateWithTooltipDirective, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 19b5113a24..3f148cab2d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -1283,7 +1283,6 @@ "maximum-upload-file-size": "ุงู„ุญุฏ ุงู„ุฃู‚ุตู‰ ู„ุญุฌู… ุงู„ู…ู„ู ุงู„ู…ุฑููˆุน: {{ size }}", "cannot-upload-file": "ู„ุง ูŠู…ูƒู† ุชุญู…ูŠู„ ุงู„ู…ู„ู", "settings": "ุงู„ุฅุนุฏุงุฏุงุช", - "layout-settings": "ุฅุนุฏุงุฏุงุช ุงู„ุชุฎุทูŠุท", "columns-count": "ุนุฏุฏ ุงู„ุฃุนู…ุฏุฉ", "columns-count-required": "ุนุฏุฏ ุงู„ุฃุนู…ุฏุฉ ู…ุทู„ูˆุจ.", "min-columns-count-message": "ุงู„ุญุฏ ุงู„ุฃุฏู†ู‰ ุงู„ู…ุณู…ูˆุญ ุจู‡ ู‡ูˆ 10 ุฃุนู…ุฏุฉ.", diff --git a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json index e043a60025..f66f248303 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json @@ -1056,7 +1056,6 @@ "maximum-upload-file-size": "Mida mร xima del fitxer de pujada: {{ size }}", "cannot-upload-file": "No s'ha pogut pujar el fitxer", "settings": "Configuraciรณ", - "layout-settings": "Parร metres de la disposiciรณ", "columns-count": "Nรบmero de columnes", "columns-count-required": "Cal nรบmero de columnes.", "min-columns-count-message": "Sol es permet un nรบmero mรญnim de 10 columnes.", diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 57e19e9faa..3be2bfdd24 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -745,7 +745,6 @@ "maximum-upload-file-size": "Maximรกlnรญ velikost souboru: {{ size }}", "cannot-upload-file": "Soubor nelze nahrรกt", "settings": "Nastavenรญ", - "layout-settings": "Nastavenรญ rozloลพenรญ", "columns-count": "Poฤet sloupcลฏ", "columns-count-required": "Poฤet sloupcลฏ je povinnรฝ.", "min-columns-count-message": "Minimรกlnรญ povolenรฝ poฤet sloupcลฏ je 10.", 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 42356ad5a1..9823c36b88 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1172,7 +1172,7 @@ "layout-type-default": "Default", "layout-type-scada": "SCADA", "layout-type-divider": "Divider", - "layout-settings": "Layout settings", + "layout-settings-type": "Layout settings: {{ type }}", "columns-count": "Columns count", "columns-count-required": "Columns count is required.", "min-columns-count-message": "Only 10 minimum column count is allowed.", @@ -1198,6 +1198,10 @@ "mobile-row-height-required": "Mobile row height value is required.", "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.", "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.", + "row-height": "Row height", + "row-height-required": "Row height value is required.", + "min-row-height-message": "Only 5 pixels is allowed as minimum row height value.", + "max-row-height-message": "Only 200 pixels is allowed as maximum row height value.", "display-first-in-mobile-view": "Display first in mobile view", "title-settings": "Title settings", "display-title": "Display dashboard title", @@ -1276,7 +1280,19 @@ "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge", "non-existent-dashboard-state-error": "Dashboard state with id \"{{ stateId }}\" is not found", "edit-mode": "Edit mode", - "duplicate-state-action": "Duplicate state" + "duplicate-state-action": "Duplicate state", + "breakpoint-value": "Breakpoint ({{ value }})", + "breakpoints-id": { + "default": "Default", + "xs": "Mobile (xs)", + "sm": "Tablet (sm)", + "md": "Laptop (md)", + "lg": "Desktop (lg)", + "xl": "Desktop (xl)" + }, + "view-format-type-grid": "Grid", + "view-format-type-list": "List", + "view-format": "View format" }, "datakey": { "settings": "Settings", @@ -5951,7 +5967,8 @@ "card-appearance": "Card appearance", "color": "Color", "tooltip": "Tooltip", - "units-required": "Unit is required." + "units-required": "Unit is required.", + "list-option": "List option" }, "widget-type": { "import": "Import widget type", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index f5f62850b2..86539fef58 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -1066,7 +1066,6 @@ "maximum-upload-file-size": "Tamaรฑo mรกximo de fichero: {{ size }}", "cannot-upload-file": "No es posible subir el fichero", "settings": "Ajustes", - "layout-settings": "Ajustes de diseรฑo", "columns-count": "Nรบmero de columnas", "columns-count-required": "Nรบmero de columnas requerido.", "min-columns-count-message": "Solo se permite un nรบmero mรญnimo de 10 columnas.", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index 33201a70b6..70f348a769 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -830,7 +830,6 @@ "empty-image": "Aucune image", "maximum-upload-file-size": "Taille de fichier maximum: {{ size }}", "cannot-upload-file": "Le tรฉlรฉchargement a รฉchouรฉ", - "layout-settings": "Paramรจtres de mise en page", "margin-required": "Valeur de marge requise.", "min-margin-message": "0 est la valeur minimum permise.", "max-margin-message": "50 est la valeur maximum permise.", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index 8b85f7f480..23e0682d6e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -1256,7 +1256,6 @@ "maximum-upload-file-size": "Maksimalus ฤฏkeliamo failo dydis: {{ size }}", "cannot-upload-file": "Failo ฤฏkelti nepavyko", "settings": "Nustatymai", - "layout-settings": "Iลกdฤ—stymo nustatymai", "columns-count": "Stulpeliลณ skaiฤius", "columns-count-required": "Stulpeliลณ skaiฤius bลซtinas.", "min-columns-count-message": "Minimalus stulpeliลณ skaiฤius - 10.", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index fb736931f4..2188379a4b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -1168,7 +1168,6 @@ "maximum-upload-file-size": "Maximale bestandsgrootte uploaden: {{ size }}", "cannot-upload-file": "Kan bestand niet uploaden", "settings": "Instellingen", - "layout-settings": "Lay-out instellingen", "columns-count": "Aantal kolommen", "columns-count-required": "Het aantal kolommen is vereist.", "min-columns-count-message": "Er is slechts een minimum van 10 kolommen toegestaan.", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 400d42c7d0..bec945acf9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -1256,7 +1256,6 @@ "maximum-upload-file-size": "Maksymalny rozmiar przesyล‚anego pliku: {{ size }}", "cannot-upload-file": "Nie moลผna przesล‚aฤ‡ pliku", "settings": "Ustawienia", - "layout-settings": "Ustawienia ukล‚adu", "columns-count": "Liczba kolumn", "columns-count-required": "Liczba kolumn jest wymagana.", "min-columns-count-message": "Dozwolona jest tylko minimalna liczba kolumn wynoszฤ…ca 10.", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index d3e63db882..b01c3548ab 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -748,7 +748,6 @@ "maximum-upload-file-size": "Maksimum yรผkleme dosyasฤฑ boyutu: {{ size }}", "cannot-upload-file": "Dosya yรผklenemiyor", "settings": "Ayarlar", - "layout-settings": "Gรถrรผnรผm ayarlarฤฑ", "columns-count": "Sรผtun sayฤฑsฤฑ", "columns-count-required": "Sรผtun sayฤฑsฤฑ gerekli.", "min-columns-count-message": "Yalnฤฑzca 10 minimum sรผtun sayฤฑsฤฑna izin verilir.", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 4ae424c843..2e3f3e0355 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1097,7 +1097,6 @@ "maximum-upload-file-size": "ๆœ€ๅคงไธŠไผ ๆ–‡ไปถๅคงๅฐ: {{ size }}", "cannot-upload-file": "ๆ— ๆณ•ไธŠไผ ๆ–‡ไปถ", "settings": "่ฎพ็ฝฎ", - "layout-settings": "ๅธƒๅฑ€่ฎพ็ฝฎ", "columns-count": "ๅˆ—ๆ•ฐ", "columns-count-required": "้œ€่ฆๅˆ—ๆ•ฐใ€‚", "min-columns-count-message": "ๅชๅ…่ฎธๆœ€ๅฐ‘10ๅˆ—", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index 68df8adcb1..959d1d2048 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -828,7 +828,6 @@ "maximum-upload-file-size": "ๆœ€ๅคงไธŠๅ‚ณๆ–‡ไปถๅคงๅฐ๏ผš {{ size }}", "cannot-upload-file": "็„กๆณ•ไธŠๅ‚ณๆ–‡ไปถ", "settings": "่จญๅฎš", - "layout-settings": "ไฝˆๅฑ€่จญๅฎš", "columns-count": "ๅˆ—ๆ•ธ", "columns-count-required": "้œ€่ฆๅˆ—ๆ•ธใ€‚", "min-columns-count-message": "ๅชๅ…่จฑๆœ€ๅฐ‘10ๅˆ—", From aa0153f30af3fcddd1844781a90872d966119df0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 16 Aug 2024 13:45:52 +0300 Subject: [PATCH 216/331] UI: Revert merge change --- .../thingsboard/server/controller/WidgetsBundleController.java | 1 - .../org/thingsboard/server/dao/model/sql/TbResourceEntity.java | 2 -- .../thingsboard/server/dao/model/sql/TbResourceInfoEntity.java | 1 - ui-ngx/src/app/core/services/dashboard-utils.service.ts | 3 ++- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index bd65dbcdc7..8d39e6c83d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -37,7 +37,6 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; -import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index ecf43eb87a..0fa74b4c9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -22,7 +22,6 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.Type; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; @@ -42,7 +41,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 2a6a3085d6..328bf38978 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -40,7 +40,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLU import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index d85ae83880..c3cead0bc2 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -18,7 +18,8 @@ import { Injectable } from '@angular/core'; import { UtilsService } from '@core/services/utils.service'; import { TimeService } from '@core/services/time.service'; import { - BreakpointId, breakpointIdIconMap, + BreakpointId, + breakpointIdIconMap, breakpointIdTranslationMap, BreakpointInfo, BreakpointLayoutInfo, From 9d287be4682b7d602e1d485b508667768ae83d13 Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 16 Aug 2024 14:19:48 +0300 Subject: [PATCH 217/331] UI: fixed oauth domain and mobile apps clients deleting --- .../admin/oauth2/domains/domain-table-config.resolver.ts | 4 ++-- .../oauth2/mobile-apps/mobile-app-table-config.resolver.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts index 39f4b5cce3..a3cc535d62 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts @@ -87,8 +87,8 @@ export class DomainTableConfigResolver implements Resolve this.domainService.getTenantDomainInfos(pageLink); this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id); this.config.saveEntity = (domain, originalDomain) => { - const clientsIds = domain.oauth2ClientInfos as Array; - if (domain.id && !isEqual(domain.oauth2ClientInfos.sort(), originalDomain.oauth2ClientInfos.map(info => info.id.id).sort())) { + const clientsIds = domain.oauth2ClientInfos as Array || []; + if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), originalDomain.oauth2ClientInfos?.map(info => info.id.id).sort())) { this.domainService.updateOauth2Clients(domain.id.id, clientsIds).subscribe(); } delete domain.oauth2ClientInfos; diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts index 96bd222ed5..b4c6c7576d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts @@ -93,8 +93,8 @@ export class MobileAppTableConfigResolver implements Resolve this.mobileAppService.getTenantMobileAppInfos(pageLink); this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); this.config.saveEntity = (mobileApp, originalMobileApp) => { - const clientsIds = mobileApp.oauth2ClientInfos as Array; - if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos.sort(), originalMobileApp.oauth2ClientInfos.map(info => info.id.id).sort())) { + const clientsIds = mobileApp.oauth2ClientInfos as Array || []; + if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), originalMobileApp.oauth2ClientInfos?.map(info => info.id.id).sort())) { this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds).subscribe(); } delete mobileApp.oauth2ClientInfos; From 367fe1b2e7431bdeff773331012168803775266f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 16 Aug 2024 16:04:14 +0300 Subject: [PATCH 218/331] UI: Added sort breakpoint in layout configuration and fix translation --- ...nage-dashboard-layouts-dialog.component.ts | 22 +++++++++++++++++++ .../select-dashboard-breakpoint.component.ts | 5 +++++ .../widget/widget-config.component.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 2 +- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts index f74a018318..9dd81de7ae 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts @@ -208,6 +208,8 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent !this.selectedBreakpointIds.includes(item.id)) .map(item => item.id); + this.sortLayoutBreakpoints(); + this.subscriptions.push( this.layoutsFormGroup.get('sliderPercentage').valueChanges .subscribe( @@ -456,6 +458,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent item.breakpoint !== breakpointId); this.allowBreakpointIds.push(breakpointId); this.selectedBreakpointIds = this.selectedBreakpointIds.filter((item) => item !== breakpointId); + this.sortLayoutBreakpoints(); this.layoutsFormGroup.markAsDirty(); } } @@ -495,6 +498,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent item !== newBreakpointId); this.addLayoutConfiguration(newBreakpointId); + this.sortLayoutBreakpoints(); } private addLayoutConfiguration(breakpointId: BreakpointId) { @@ -508,4 +512,22 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent { + const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a.breakpoint)?.maxWidth || Infinity; + const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b.breakpoint)?.maxWidth || Infinity; + return bMaxWidth - aMaxWidth; + }); + this.selectedBreakpointIds.sort((a, b) => { + const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity; + const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity; + return bMaxWidth - aMaxWidth; + }); + this.allowBreakpointIds.sort((a, b) => { + const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity; + const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity; + return bMaxWidth - aMaxWidth; + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts index b50e7130d6..9caa433b0a 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts @@ -43,6 +43,11 @@ export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy { this.layoutDataChanged$ = this.dashboardCtrl.layouts.main.layoutCtx.layoutDataChanged.subscribe(() => { if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) { this.breakpointIds = Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData); + this.breakpointIds.sort((a, b) => { + const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity; + const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity; + return bMaxWidth - aMaxWidth; + }); if (this.breakpointIds.indexOf(this.dashboardCtrl.layouts.main.layoutCtx.breakpoint) > -1) { this.selectedBreakpoint = this.dashboardCtrl.layouts.main.layoutCtx.breakpoint; } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 2bcc4b04e2..89cb4040b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -354,7 +354,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe { name: this.isDefaultBreakpoint ? this.translate.instant('widget-config.mobile') - : this.translate.instant('widget-config.list-option'), + : this.translate.instant('widget-config.layout-option'), value: 'mobile' } ); 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 9823c36b88..fabb3bf1ef 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5968,7 +5968,7 @@ "color": "Color", "tooltip": "Tooltip", "units-required": "Unit is required.", - "list-option": "List option" + "layout-option": "Layout option" }, "widget-type": { "import": "Import widget type", From bcf28c94e9f6d116975f2282eb11b81b038d5342 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 16 Aug 2024 16:21:16 +0300 Subject: [PATCH 219/331] added implementation for dashboards and widget edit; added widget bundle export --- ui-ngx/src/app/core/http/widget.service.ts | 3 +- .../dashboard-page.component.ts | 13 +++- .../entity/entity-details-panel.component.ts | 3 +- .../widget/widget-component.service.ts | 2 +- .../home/models/widget-component.models.ts | 7 ++- .../pages/widget/widget-editor.component.ts | 15 +++-- .../entity-conflict-dialog.component.scss | 2 +- ...xport-widgets-bundle-dialog.component.html | 8 +-- .../export-widgets-bundle-dialog.component.ts | 4 ++ .../import-export/import-export.service.ts | 63 ++++++++++++------- ui-ngx/src/app/shared/models/widget.models.ts | 4 +- 11 files changed, 81 insertions(+), 43 deletions(-) diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index b172b61a1d..8b57feb488 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -185,8 +185,9 @@ export class WidgetService { public saveWidgetTypeDetails(widgetInfo: WidgetInfo, id: WidgetTypeId, createdTime: number, + version: number, config?: RequestConfig): Observable { - const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime); + const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime, version); return this.http.post('/api/widgetType', widgetTypeDetails, defaultHttpOptionsFromConfig(config)).pipe( tap((savedWidgetType) => { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index d16e8f360f..2609744cfc 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -146,7 +146,7 @@ import { IAliasController } from '@core/api/widget-api.models'; import { MatButton } from '@angular/material/button'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { TbPopoverService } from '@shared/components/popover.service'; -import { map, tap } from 'rxjs/operators'; +import { catchError, map, tap } from 'rxjs/operators'; import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { ResizeObserver } from '@juggle/resize-observer'; @@ -1027,7 +1027,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC public saveDashboard() { this.translatedDashboardTitle = this.getTranslatedDashboardTitle(); - this.setEditMode(false, false); this.notifyDashboardUpdated(); } @@ -1146,7 +1145,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }; this.window.parent.postMessage(JSON.stringify(message), '*'); } else { - this.dashboardService.saveDashboard(this.dashboard).subscribe(); + this.dashboardService.saveDashboard(this.dashboard).pipe( + catchError(() => { + this.setEditMode(false, true); + return of(null); + }) + ).subscribe(() => { + this.dashboard.version = this.dashboard.version + 1; + this.setEditMode(false, false); + }); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index 6a1e8f5f47..c283a57c4f 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -44,7 +44,7 @@ import { Observable, of, ReplaySubject, Subscription } from 'rxjs'; import { MatTab, MatTabGroup } from '@angular/material/tabs'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; import { deepClone, mergeDeep } from '@core/utils'; -import { catchError, take } from 'rxjs/operators'; +import { catchError } from 'rxjs/operators'; @Component({ selector: 'tb-entity-details-panel', @@ -290,7 +290,6 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV } this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity) .pipe( - take(1), catchError(() => of(this.entity)) ) .subscribe( diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 23d0403a6e..aa7f62e33b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -113,7 +113,7 @@ export class WidgetComponentService { hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, defaultConfig: this.utils.editWidgetInfo.defaultConfig - }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined, null ); } const initSubject = new ReplaySubject(); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 1441bd4e92..85b10124e8 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -665,7 +665,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails): }; export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, - createdTime: number): WidgetType => { + createdTime: number, version: number): WidgetType => { const descriptor: WidgetTypeDescriptor = { type: widgetInfo.type, sizeX: widgetInfo.sizeX, @@ -688,6 +688,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: id, tenantId, createdTime, + version, fqn: widgetTypeFqn(widgetInfo.fullFqn), name: widgetInfo.widgetName, deprecated: widgetInfo.deprecated, @@ -697,8 +698,8 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: }; export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, - createdTime: number): WidgetTypeDetails => { - const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime); + createdTime: number, version: number): WidgetTypeDetails => { + const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime, version); return { ...widgetTypeEntity, description: widgetInfo.description, diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 4e470d03da..614787233d 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -63,7 +63,7 @@ import { forkJoin, mergeMap, of, Subscription } from 'rxjs'; import { ResizeObserver } from '@juggle/resize-observer'; import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; import { Observable } from 'rxjs/internal/Observable'; -import { map, tap } from 'rxjs/operators'; +import { catchError, map, tap } from 'rxjs/operators'; import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; import Timeout = NodeJS.Timeout; @@ -569,9 +569,12 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe private commitSaveWidget() { const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined; + const version = this.widgetTypeDetails?.version ?? null; const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined; - this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime).pipe( + this.saveWidgetPending = false; + this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime, version).pipe( mergeMap((widgetTypeDetails) => { + this.saveWidgetPending = true; const widgetsBundleId = this.route.snapshot.params.widgetsBundleId as string; if (widgetsBundleId && !id) { return this.widgetService.addWidgetFqnToWidgetBundle(widgetsBundleId, widgetTypeDetails.fqn).pipe( @@ -579,7 +582,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe ); } return of(widgetTypeDetails); - }) + }), + catchError(() => { + this.undoWidget(); + return of(null); + }), ).subscribe({ next: (widgetTypeDetails) => { this.saveWidgetPending = false; @@ -612,7 +619,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe config.title = this.widget.widgetName; this.widget.defaultConfig = JSON.stringify(config); this.isDirty = false; - this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).pipe( + this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined, null).pipe( mergeMap((widget) => { if (saveWidgetAsData.widgetBundleId) { return this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, widget.fqn).pipe( diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss index cb1540c341..02c2e6bd00 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -$conflict-dialog-width: 520px; +$conflict-dialog-width: 530px; :host { .main-label { diff --git a/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.html b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.html index f8ddefc2ca..27aaeec20b 100644 --- a/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.html @@ -24,23 +24,23 @@ close - +
-
+
{{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }}
diff --git a/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts index 025a2ddf4c..f012dc77fe 100644 --- a/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts +++ b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts @@ -27,6 +27,7 @@ import { isDefinedAndNotNull } from '@core/utils'; export interface ExportWidgetsBundleDialogData { widgetsBundle: WidgetsBundle; includeBundleWidgetsInExport: boolean; + ignoreLoading?: boolean; } export interface ExportWidgetsBundleDialogResult { @@ -44,6 +45,8 @@ export class ExportWidgetsBundleDialogComponent extends DialogComponent, @@ -52,6 +55,7 @@ export class ExportWidgetsBundleDialogComponent extends DialogComponent) { super(store, router, dialogRef); this.widgetsBundle = data.widgetsBundle; + this.ignoreLoading = data.ignoreLoading; if (isDefinedAndNotNull(data.includeBundleWidgetsInExport)) { this.exportWidgetsFormControl.patchValue(data.includeBundleWidgetsInExport, {emitEvent: false}); } diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 6ca925e9e9..682b6122f0 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -351,28 +351,7 @@ export class ImportExportService { forkJoin(tasks).subscribe({ next: ({includeBundleWidgetsInExport, widgetsBundle}) => { - this.dialog.open(ExportWidgetsBundleDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - widgetsBundle, - includeBundleWidgetsInExport - } - }).afterClosed().subscribe( - (result) => { - if (result) { - if (includeBundleWidgetsInExport !== result.exportWidgets) { - this.store.dispatch(new ActionPreferencesPutUserSettings({includeBundleWidgetsInExport: result.exportWidgets})); - } - if (result.exportWidgets) { - this.exportWidgetsBundleWithWidgetTypes(widgetsBundle); - } else { - this.exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle); - } - } - } - ); + this.handleExportWidgetsBundle(widgetsBundle, includeBundleWidgetsInExport); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -400,6 +379,9 @@ export class ImportExportService { })) .subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name)); return; + case EntityType.WIDGETS_BUNDLE: + this.exportSelectedWidgetsBundle(entityData as WidgetsBundle); + return; case EntityType.DASHBOARD: preparedData = this.prepareDashboardExport(entityData as Dashboard); break; @@ -409,6 +391,43 @@ export class ImportExportService { this.exportToPc(preparedData, entityData.name); } + private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void { + this.store.pipe(select(selectUserSettingsProperty( 'includeBundleWidgetsInExport'))).pipe(take(1)).subscribe({ + next: (includeBundleWidgetsInExport) => { + this.handleExportWidgetsBundle(widgetsBundle, includeBundleWidgetsInExport, true); + }, + error: (e) => { + this.handleExportError(e, 'widgets-bundle.export-failed-error'); + } + }); + } + + private handleExportWidgetsBundle(widgetsBundle: WidgetsBundle, includeBundleWidgetsInExport: boolean, ignoreLoading?: boolean): void { + this.dialog.open(ExportWidgetsBundleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + widgetsBundle, + includeBundleWidgetsInExport, + ignoreLoading + } + }).afterClosed().subscribe( + (result) => { + if (result) { + if (includeBundleWidgetsInExport !== result.exportWidgets) { + this.store.dispatch(new ActionPreferencesPutUserSettings({includeBundleWidgetsInExport: result.exportWidgets})); + } + if (result.exportWidgets) { + this.exportWidgetsBundleWithWidgetTypes(widgetsBundle); + } else { + this.exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle); + } + } + } + ); + } + private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) { this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe({ next: (widgetTypesDetails) => { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 977de00ce7..3b60cd727e 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -41,7 +41,7 @@ import { isNotEmptyStr, mergeDeepIgnoreArray } from '@core/utils'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; -import { HasTenantId } from '@shared/models/entity.models'; +import { HasTenantId, HasVersion } from '@shared/models/entity.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; @@ -199,7 +199,7 @@ export interface WidgetControllerDescriptor { actionSources?: {[actionSourceId: string]: WidgetActionSource}; } -export interface BaseWidgetType extends BaseData, HasTenantId { +export interface BaseWidgetType extends BaseData, HasTenantId, HasVersion { tenantId: TenantId; fqn: string; name: string; From f5f679479a9a612143c69005b9bd2f7f247c9a2b Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 16 Aug 2024 16:30:35 +0300 Subject: [PATCH 220/331] UI: Fix translation --- .../modules/home/components/widget/widget-config.component.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 89cb4040b4..f554c4bf3f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -354,7 +354,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe { name: this.isDefaultBreakpoint ? this.translate.instant('widget-config.mobile') - : this.translate.instant('widget-config.layout-option'), + : this.translate.instant('widget-config.list-layout'), value: 'mobile' } ); 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 fabb3bf1ef..42a2398928 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5968,7 +5968,7 @@ "color": "Color", "tooltip": "Tooltip", "units-required": "Unit is required.", - "layout-option": "Layout option" + "list-layout": "List layout" }, "widget-type": { "import": "Import widget type", From 74238cd40ec9278d03270b0defdc5983e0e46b19 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 16 Aug 2024 16:38:05 +0300 Subject: [PATCH 221/331] UI: Fix show widget action in tooltip --- .../home/components/widget/widget-container.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index d1332c1bd7..0bf5bb0327 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -256,7 +256,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O $(this.gridsterItem.el).tooltipster({ delay: this.widget.selected ? [0, 10000000] : [0, 100], distance: 2, - zIndex: 151, + zIndex: 1000, arrow: false, theme: ['tb-widget-edit-actions-tooltip'], interactive: true, From d67159a491bef8588d4e58394fd66b57c8c3ae35 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 16 Aug 2024 17:42:55 +0300 Subject: [PATCH 222/331] UI: Fix show widget action in tooltip --- .../components/dashboard-page/dashboard-page.models.ts | 3 +++ .../home/components/widget/widget-container.component.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index 540fd0d69b..8ce2ab0679 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -29,6 +29,7 @@ import { ILayoutController } from './layout/layout.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; import { BehaviorSubject, Observable } from 'rxjs'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; +import { ElementRef } from '@angular/core'; export declare type DashboardPageScope = 'tenant' | 'customer'; @@ -54,6 +55,8 @@ export interface DashboardContext { export interface IDashboardController { dashboardCtx: DashboardContext; + dashboardContainer: ElementRef; + elRef: ElementRef; openRightLayout(); openDashboardState(stateId: string, openRightLayout: boolean); addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 0bf5bb0327..698b78eaa7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -253,10 +253,12 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O private initEditWidgetActionTooltip() { from(import('tooltipster')).subscribe(() => { + const dashboardElement = this.widget.widgetContext.dashboard.stateController.dashboardCtrl.elRef.nativeElement; $(this.gridsterItem.el).tooltipster({ + parent: $(dashboardElement), delay: this.widget.selected ? [0, 10000000] : [0, 100], distance: 2, - zIndex: 1000, + zIndex: 151, arrow: false, theme: ['tb-widget-edit-actions-tooltip'], interactive: true, @@ -273,7 +275,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O content: '', functionPosition: (instance, helper, position) => { const clientRect = helper.origin.getBoundingClientRect(); - position.coord.left = clientRect.right - position.size.width; + const container = dashboardElement.getBoundingClientRect(); + position.coord.left = clientRect.right - position.size.width - container.left; + position.coord.top = position.coord.top - container.top; position.target = clientRect.right; return position; }, From 931102f73c78b5b0de6b1690e3f4558cf3e81822 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 16 Aug 2024 18:00:34 +0300 Subject: [PATCH 223/331] fixed pull request comments --- .../main/data/upgrade/3.7.0/schema_update.sql | 18 +++- ...tomOAuth2AuthorizationRequestResolver.java | 11 ++- .../server/controller/BaseController.java | 6 +- .../server/controller/DomainController.java | 6 +- .../controller/MobileAppController.java | 15 ++- .../server/controller/OAuth2Controller.java | 28 +++--- .../domain/DefaultTbDomainService.java | 10 +- .../mobile/DefaultTbMobileAppService.java | 6 +- .../DefaultTbOauth2ClientService.java | 6 +- .../DefaultSystemDataLoaderService.java | 6 +- .../oauth2/AbstractOAuth2ClientMapper.java | 10 +- .../auth/oauth2/AppleOAuth2ClientMapper.java | 6 +- .../server/controller/AbstractWebTest.java | 6 +- .../controller/DomainControllerTest.java | 6 +- .../server/controller/HomePageApiTest.java | 2 +- .../controller/MobileAppControllerTest.java | 6 +- .../Oauth2ClientControllerTest.java | 2 +- .../dao/oauth2/OAuth2ClientService.java | 2 +- .../data/domain/DomainOauth2Client.java | 2 + .../common/data/mobile/MobileAppInfo.java | 2 +- .../common/data/oauth2/OAuth2Client.java | 4 +- .../common/data/oauth2/OAuth2ClientInfo.java | 5 +- .../server/dao/domain/DomainDao.java | 5 +- .../server/dao/domain/DomainServiceImpl.java | 94 ++++++++----------- .../server/dao/mobile/MobileAppDao.java | 4 +- .../dao/mobile/MobileAppServiceImpl.java | 74 ++++++--------- .../server/dao/model/ModelConstants.java | 4 +- .../server/dao/model/sql/DomainEntity.java | 2 +- .../server/dao/oauth2/OAuth2ClientDao.java | 4 +- .../dao/oauth2/OAuth2ClientServiceImpl.java | 25 ++--- .../domain/DomainOauth2ClientRepository.java | 3 +- .../dao/sql/domain/DomainRepository.java | 2 +- .../server/dao/sql/domain/JpaDomainDao.java | 7 +- .../dao/sql/mobile/JpaMobileAppDao.java | 10 +- .../MobileAppOauth2ClientRepository.java | 3 +- .../dao/sql/oauth2/JpaOAuth2ClientDao.java | 6 +- .../sql/oauth2/OAuth2ClientRepository.java | 8 +- .../server/dao/tenant/TenantServiceImpl.java | 1 + .../main/resources/sql/schema-entities.sql | 2 +- 39 files changed, 193 insertions(+), 226 deletions(-) diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index dce84e3a40..5bb1a61f28 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -18,6 +18,7 @@ ALTER TABLE IF EXISTS oauth2_mobile RENAME TO mobile_app; ALTER TABLE IF EXISTS oauth2_domain RENAME TO domain; +ALTER TABLE IF EXISTS oauth2_domain RENAME domain_name TO name; ALTER TABLE IF EXISTS oauth2_registration RENAME TO oauth2_client; ALTER TABLE domain ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, @@ -26,13 +27,21 @@ ALTER TABLE domain ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, DROP COLUMN IF EXISTS domain_scheme; -- delete duplicated domains -DELETE FROM domain d1 USING domain d2 WHERE d1.created_time < d2.created_time AND d1.domain_name = d2.domain_name; +DELETE FROM domain d1 USING ( + SELECT MIN(ctid) as ctid, name + FROM domain + GROUP BY name HAVING COUNT(*) > 1 +) d2 WHERE d1.name = d2.name AND d1.ctid <> d2.ctid; ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080'; -- delete duplicated apps -DELETE FROM mobile_app m1 USING mobile_app m2 WHERE m1.created_time < m2.created_time AND m1.pkg_name = m2.pkg_name; +DELETE FROM mobile_app m1 USING ( + SELECT MIN(ctid) as ctid, pkg_name + FROM mobile_app + GROUP BY pkg_name HAVING COUNT(*) > 1 +) m2 WHERE m1.pkg_name = m2.pkg_name AND m1.ctid <> m2.ctid; ALTER TABLE oauth2_client ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080', ADD COLUMN IF NOT EXISTS title varchar(100); @@ -81,9 +90,12 @@ $$ ALTER TABLE oauth2_client DROP COLUMN oauth2_params_id; ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_unq_key UNIQUE (pkg_name); - ALTER TABLE domain ADD CONSTRAINT domain_unq_key UNIQUE (domain_name); + ALTER TABLE domain ADD CONSTRAINT domain_unq_key UNIQUE (name); DROP TABLE IF EXISTS oauth2_params; + -- drop deprecated tables + DROP TABLE IF EXISTS oauth2_client_registration_info; + DROP TABLE IF EXISTS oauth2_client_registration; END IF; END $$; diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index 2dd5027a2a..542a7c71cb 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -37,6 +37,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -115,14 +116,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza return request.getParameter("appToken"); } - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) { - if (registrationId == null) { + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String oauth2ClientId, String redirectUriAction, String appPackage, String appToken) { + if (oauth2ClientId == null) { return null; } - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(oauth2ClientId); if (clientRegistration == null) { - throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + throw new IllegalArgumentException("Invalid Client Registration with Id: " + oauth2ClientId); } Map attributes = new HashMap<>(); @@ -131,7 +132,7 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza if (StringUtils.isEmpty(appToken)) { throw new IllegalArgumentException("Invalid application token."); } else { - String appSecret = this.oAuth2ClientService.findAppSecret(UUID.fromString(registrationId), appPackage); + String appSecret = this.oAuth2ClientService.findAppSecret(new OAuth2ClientId(UUID.fromString(oauth2ClientId)), appPackage); if (StringUtils.isEmpty(appSecret)) { throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package."); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index f45e687b8d..7750376a75 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -901,9 +901,11 @@ public abstract class BaseController { } protected List getOAuth2ClientIds(UUID[] ids) throws ThingsboardException { - List oauth2ClientIds = ids != null ? Arrays.asList(ids) : Collections.emptyList(); + if (ids == null) { + return Collections.emptyList(); + } List oAuth2ClientIds = new ArrayList<>(); - for (UUID id : oauth2ClientIds) { + for (UUID id : ids) { OAuth2ClientId oauth2ClientId = new OAuth2ClientId(id); checkOauth2ClientId(oauth2ClientId, Operation.READ); oAuth2ClientIds.add(oauth2ClientId); diff --git a/application/src/main/java/org/thingsboard/server/controller/DomainController.java b/application/src/main/java/org/thingsboard/server/controller/DomainController.java index 79326f56bc..fa1f639281 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DomainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DomainController.java @@ -76,7 +76,7 @@ public class DomainController extends BaseController { @RequestBody @Valid Domain domain, @Parameter(description = "A list of oauth2 client registration ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception { - domain.setTenantId(getCurrentUser().getTenantId()); + domain.setTenantId(getTenantId()); checkEntity(domain.getId(), domain, Resource.DOMAIN); return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser()); } @@ -100,7 +100,7 @@ public class DomainController extends BaseController { @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on rule's name") + @Parameter(description = "Case-insensitive 'substring' filter based on domain's name") @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION) @RequestParam(required = false) String sortProperty, @@ -119,7 +119,7 @@ public class DomainController extends BaseController { } @ApiOperation(value = "Delete Domain by ID (deleteDomain)", - notes = "Deletes Domain by ID. Referencing non-existing asset Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + notes = "Deletes Domain by ID. Referencing non-existing domain Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @DeleteMapping(value = "/domain/{id}") public void deleteDomain(@PathVariable UUID id) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 4d0f19299f..6620e854d0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -75,19 +75,19 @@ public class MobileAppController extends BaseController { public MobileApp saveMobileApp( @Parameter(description = "A JSON value representing the Mobile Application.", required = true) @RequestBody @Valid MobileApp mobileApp, - @Parameter(description = "A list of entity group ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @Parameter(description = "A list of entity oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception { - mobileApp.setTenantId(getCurrentUser().getTenantId()); + mobileApp.setTenantId(getTenantId()); checkEntity(mobileApp.getId(), mobileApp, Resource.MOBILE_APP); return tbMobileAppService.save(mobileApp, getOAuth2ClientIds(ids), getCurrentUser()); } @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", - notes = "Update oauth2 clients to the specified mobile app. ") + notes = "Update oauth2 clients of the specified mobile app. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PutMapping(value = "/mobileApp/{id}/oauth2Clients") public void updateOauth2Clients(@PathVariable UUID id, - @RequestBody UUID[] clientIds) throws ThingsboardException { + @RequestBody UUID[] clientIds) throws ThingsboardException { MobileAppId mobileAppId = new MobileAppId(id); MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.WRITE); List oAuth2ClientIds = getOAuth2ClientIds(clientIds); @@ -101,15 +101,14 @@ public class MobileAppController extends BaseController { @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on rule's name") + @Parameter(description = "Case-insensitive 'substring' filter based on app's name") @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return mobileAppService.findMobileAppInfosByTenantId(tenantId, pageLink); + return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink); } @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @@ -121,7 +120,7 @@ public class MobileAppController extends BaseController { } @ApiOperation(value = "Delete Mobile App by ID (deleteMobileApp)", - notes = "Deletes Mobile App by ID. Referencing non-existing asset Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @DeleteMapping(value = "/mobileApp/{id}") public void deleteMobileApp(@PathVariable UUID id) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index ee46438569..4d0dc61750 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -84,7 +84,7 @@ public class OAuth2Controller extends BaseController { "the usage with this platform type is allowed in the settings. " + "If platform type is not one of allowable values - it will just be ignored", schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"})) - @RequestParam(required = false) String platform) throws ThingsboardException { + @RequestParam(required = false) String platform) { if (log.isDebugEnabled()) { log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); Enumeration headerNames = request.getHeaderNames(); @@ -95,10 +95,7 @@ public class OAuth2Controller extends BaseController { } PlatformType platformType = null; if (StringUtils.isNotEmpty(platform)) { - try { - platformType = PlatformType.valueOf(platform); - } catch (Exception e) { - } + platformType = PlatformType.valueOf(platform); } if (StringUtils.isNotEmpty(pkgName)) { return oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platformType); @@ -117,14 +114,14 @@ public class OAuth2Controller extends BaseController { return tbOauth2ClientService.save(oAuth2Client, getCurrentUser()); } - @ApiOperation(value = "Get OAuth2 Client Registration infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/oauth2/client/infos") public PageData findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on rule's name") + @Parameter(description = "Case-insensitive 'substring' filter based on client's title") @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION) @RequestParam(required = false) String sortProperty, @@ -134,22 +131,21 @@ public class OAuth2Controller extends BaseController { return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } - @ApiOperation(value = "Get OAuth2 Client Registration infos By Ids (findTenantOAuth2ClientInfosByIds)", - notes = "Fetch OAuth2 Client Registration info objects based on the provided ids. ") + @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", + notes = "Fetch OAuth2 Client info objects based on the provided ids. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) public List findTenantOAuth2ClientInfosByIds( @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("clientIds") String[] strClientIds) throws ThingsboardException { - checkArrayParameter("clientIds", strClientIds); + @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { List oAuth2ClientIds = new ArrayList<>(); - for (String oauth2ClientId : strClientIds) { - oAuth2ClientIds.add(new OAuth2ClientId(toUUID(oauth2ClientId))); + for (UUID oauth2ClientId : clientIds) { + oAuth2ClientIds.add(new OAuth2ClientId(oauth2ClientId)); } return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); } - @ApiOperation(value = "Get OAuth2 Client Registration by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/oauth2/client/{id}") public OAuth2Client getOAuth2ClientById(@PathVariable UUID id) throws ThingsboardException { @@ -157,8 +153,8 @@ public class OAuth2Controller extends BaseController { return checkEntityId(oAuth2ClientId, oAuth2ClientService::findOAuth2ClientById, Operation.READ); } - @ApiOperation(value = "Delete oauth2 client (deleteAsset)", - notes = "Deletes the asset and all the relations (from and to the asset). Referencing non-existing asset Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Delete oauth2 client (deleteOauth2Client)", + notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @DeleteMapping(value = "/oauth2/client/{id}") public void deleteOauth2Client(@PathVariable UUID id) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java index 755972f9fa..59cf6f9856 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java @@ -16,9 +16,8 @@ package org.thingsboard.server.service.entitiy.domain; import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -46,7 +45,7 @@ public class DefaultTbDomainService extends AbstractTbEntityService implements T if (!CollectionUtils.isEmpty(oAuth2Clients)) { domainService.updateOauth2Clients(domain.getTenantId(), savedDomain.getId(), oAuth2Clients); } - logEntityActionService.logEntityAction(tenantId, savedDomain.getId(), domain, actionType, user, oAuth2Clients); + logEntityActionService.logEntityAction(tenantId, savedDomain.getId(), savedDomain, actionType, user, oAuth2Clients); return savedDomain; } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.DOMAIN), domain, actionType, user, e, oAuth2Clients); @@ -61,15 +60,14 @@ public class DefaultTbDomainService extends AbstractTbEntityService implements T DomainId domainId = domain.getId(); try { domainService.updateOauth2Clients(tenantId, domainId, oAuth2ClientIds); - logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, oAuth2ClientIds.toString()); + logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, oAuth2ClientIds); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, e, oAuth2ClientIds.toString()); + logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, e, oAuth2ClientIds); throw e; } } @Override - @Transactional public void delete(Domain domain, User user) { ActionType actionType = ActionType.DELETED; TenantId tenantId = domain.getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java index a0731789be..7d5f1122da 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java @@ -16,9 +16,8 @@ package org.thingsboard.server.service.entitiy.mobile; import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -46,7 +45,7 @@ public class DefaultTbMobileAppService extends AbstractTbEntityService implement if (!CollectionUtils.isEmpty(oauth2Clients)) { mobileAppService.updateOauth2Clients(tenantId, savedMobileApp.getId(), oauth2Clients); } - logEntityActionService.logEntityAction(tenantId, savedMobileApp.getId(), mobileApp, actionType, user); + logEntityActionService.logEntityAction(tenantId, savedMobileApp.getId(), savedMobileApp, actionType, user); return savedMobileApp; } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.MOBILE_APP), mobileApp, actionType, user, e); @@ -69,7 +68,6 @@ public class DefaultTbMobileAppService extends AbstractTbEntityService implement } @Override - @Transactional public void delete(MobileApp mobileApp, User user) { ActionType actionType = ActionType.DELETED; TenantId tenantId = mobileApp.getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java index 0a36c23369..999bf3976c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java @@ -37,9 +37,9 @@ public class DefaultTbOauth2ClientService extends AbstractTbEntityService implem ActionType actionType = oAuth2Client.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = oAuth2Client.getTenantId(); try { - OAuth2Client savedRegistration = checkNotNull(oAuth2ClientService.saveOAuth2Client(tenantId, oAuth2Client)); - logEntityActionService.logEntityAction(tenantId, savedRegistration.getId(), oAuth2Client, actionType, user); - return savedRegistration; + OAuth2Client savedClient = checkNotNull(oAuth2ClientService.saveOAuth2Client(tenantId, oAuth2Client)); + logEntityActionService.logEntityAction(tenantId, savedClient.getId(), savedClient, actionType, user); + return savedClient; } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.OAUTH2_CLIENT), oAuth2Client, actionType, user, e); throw e; diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index dcb19ed271..92c5909056 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -149,7 +149,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final DeviceConnectivityConfiguration connectivityConfiguration; private final QueueService queueService; private final JwtSettingsService jwtSettingsService; - private final MobileAppDao oAuth2MobileDao; + private final MobileAppDao mobileAppDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; @@ -308,7 +308,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { jwtSettingsService.saveJwtSettings(jwtSettings); } - List mobiles = oAuth2MobileDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData(); + List mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData(); if (CollectionUtils.isNotEmpty(mobiles)) { mobiles.stream() .filter(config -> !validateKeyLength(config.getAppSecret())) @@ -318,7 +318,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { "You can change the Application Secret using the Web UI: " + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", config.getPkgName()); config.setAppSecret(generateRandomKey()); - oAuth2MobileDao.save(TenantId.SYS_TENANT_ID, config); + mobileAppDao.save(TenantId.SYS_TENANT_ID, config); }); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index 0182d5026c..a4960182b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -97,9 +97,9 @@ public abstract class AbstractOAuth2ClientMapper { private final Lock userCreationLock = new ReentrantLock(); - protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2Client registration) { + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2Client oAuth2Client) { - OAuth2MapperConfig config = registration.getMapperConfig(); + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); @@ -143,9 +143,9 @@ public abstract class AbstractOAuth2ClientMapper { } } - if (registration.getAdditionalInfo() != null && - registration.getAdditionalInfo().has("providerName")) { - additionalInfo.put("authProviderName", registration.getAdditionalInfo().get("providerName").asText()); + if (oAuth2Client.getAdditionalInfo() != null && + oAuth2Client.getAdditionalInfo().has("providerName")) { + additionalInfo.put("authProviderName", oAuth2Client.getAdditionalInfo().get("providerName").asText()); } user.setAdditionalInfo(additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java index 0e4ee821f7..c7a499c7ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java @@ -45,13 +45,13 @@ public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen private static final String EMAIL = "email"; @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); Map attributes = updateAttributesFromRequestParams(request, token.getPrincipal().getAttributes()); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, oAuth2Client); } private static Map updateAttributesFromRequestParams(HttpServletRequest request, Map attributes) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 59c1a50fa1..de3de1db7e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1114,11 +1114,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile); } - protected OAuth2Client validClientInfo(TenantId tenantId, String title) { - return validClientInfo(tenantId, title, null); + protected OAuth2Client createOauth2Client(TenantId tenantId, String title) { + return createOauth2Client(tenantId, title, null); } - protected OAuth2Client validClientInfo(TenantId tenantId, String title, List platforms) { + protected OAuth2Client createOauth2Client(TenantId tenantId, String title, List platforms) { OAuth2Client oAuth2Client = new OAuth2Client(); oAuth2Client.setTenantId(tenantId); oAuth2Client.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java index 609444bd3d..c899aeeddb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java @@ -98,10 +98,10 @@ public class DomainControllerTest extends AbstractControllerTest { Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); Domain savedDomain = doPost("/api/domain", domain, Domain.class); - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); - OAuth2Client oAuth2Client2 = validClientInfo(TenantId.SYS_TENANT_ID, "test facebook client"); + OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client"); OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class); doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); @@ -117,7 +117,7 @@ public class DomainControllerTest extends AbstractControllerTest { @Test public void testCreateDomainWithOauth2Clients() throws Exception { - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index 6b46e64cfe..69f3c2a7db 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -360,7 +360,7 @@ public class HomePageApiTest extends AbstractControllerTest { Assert.assertTrue(featuresInfo.isNotificationEnabled()); Assert.assertFalse(featuresInfo.isOauthEnabled()); - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); Domain domain = createDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java index a0deb39a51..23b1f47c91 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java @@ -100,10 +100,10 @@ public class MobileAppControllerTest extends AbstractControllerTest { MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); MobileApp savedMobileApp = doPost("/api/mobileApp", mobileApp, MobileApp.class); - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); - OAuth2Client oAuth2Client2 = validClientInfo(TenantId.SYS_TENANT_ID, "test facebook client"); + OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client"); OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class); doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); @@ -119,7 +119,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { @Test public void testCreateMobileAppWithOauth2Clients() throws Exception { - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); diff --git a/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java index fbde35567c..4e8ac72676 100644 --- a/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java @@ -57,7 +57,7 @@ public class Oauth2ClientControllerTest extends AbstractControllerTest { PageData pageData = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); assertThat(pageData.getData()).isEmpty(); - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); PageData pageData2 = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java index a195e50917..e4c67108db 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java @@ -40,7 +40,7 @@ public interface OAuth2ClientService extends EntityDaoService { OAuth2Client findOAuth2ClientById(TenantId tenantId, OAuth2ClientId providerId); - String findAppSecret(UUID registrationId, String pkgName); + String findAppSecret(OAuth2ClientId oAuth2ClientId, String pkgName); void deleteOAuth2ClientById(TenantId tenantId, OAuth2ClientId oAuth2ClientId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java index 11c872c8be..91cbb8a7a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.domain; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.OAuth2ClientId; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.id.OAuth2ClientId; @Data @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode public class DomainOauth2Client { private DomainId domainId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java index 9a0b0b3b1c..4d85028e36 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java @@ -28,7 +28,7 @@ import java.util.List; @Schema public class MobileAppInfo extends MobileApp { - @Schema(description = "List of available oauth2 client registrations") + @Schema(description = "List of available oauth2 clients") private List oauth2ClientInfos; public MobileAppInfo(MobileApp mobileApp, List oauth2ClientInfos) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java index 7624a6ccd5..f9b93b00e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import java.util.List; @@ -41,6 +42,7 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp private TenantId tenantId; @Schema(description = "Oauth2 client title") @NotBlank + @Length(max = 100, message = "cannot be longer than 100 chars") private String title; @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull @@ -110,6 +112,6 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { - return loginButtonLabel; + return title; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java index c2e9bce7be..4e083545c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.oauth2; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -49,9 +50,7 @@ public class OAuth2ClientInfo extends BaseData implements HasNam public OAuth2ClientInfo(OAuth2Client oAuth2Client) { super(oAuth2Client); this.title = oAuth2Client.getTitle(); - if (oAuth2Client.getAdditionalInfo() != null && oAuth2Client.getAdditionalInfo().has("providerName")) { - this.providerName = oAuth2Client.getAdditionalInfo().get("providerName").asText(); - } + this.providerName = oAuth2Client.getAdditionalInfoField("providerName", JsonNode::asText,""); this.platforms = oAuth2Client.getPlatforms(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java index cbc371520c..e4eaf57356 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.domain; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.domain.DomainOauth2Client; import org.thingsboard.server.common.data.id.DomainId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -34,9 +33,9 @@ public interface DomainDao extends Dao { List findOauth2ClientsByDomainId(TenantId tenantId, DomainId domainId); - void saveOauth2Clients(DomainOauth2Client domainOauth2Client); + void addOauth2Client(DomainOauth2Client domainOauth2Client); - void removeOauth2Clients(DomainId domainId, OAuth2ClientId oAuth2ClientId); + void removeOauth2Client(DomainOauth2Client domainOauth2Client); void deleteByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java index 196a9324e7..487e3feb5c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java @@ -16,11 +16,9 @@ package org.thingsboard.server.dao.domain; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.domain.DomainInfo; @@ -36,24 +34,21 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; -import org.thingsboard.server.dao.service.Validator; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; -import static org.thingsboard.server.dao.service.Validator.validateIds; +import static org.thingsboard.server.dao.service.Validator.validateId; @Slf4j @Service public class DomainServiceImpl extends AbstractEntityService implements DomainService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_DOMAIN_ID = "Incorrect domainId "; @Autowired private OAuth2ClientDao oauth2ClientDao; @@ -67,39 +62,31 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe Domain savedDomain = domainDao.save(tenantId, domain); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedDomain).build()); return savedDomain; - } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("domain_unq_key")) { - throw new DataValidationException("Domain with such name and scheme already exists!"); - } else { - throw t; - } + } catch (Exception e) { + checkConstraintViolation(e, + Map.of("domain_unq_key", "Domain with such name and scheme already exists!")); + throw e; } } @Override public void updateOauth2Clients(TenantId tenantId, DomainId domainId, List oAuth2ClientIds) { log.trace("Executing updateOauth2Clients, domainId [{}], oAuth2ClientIds [{}]", domainId, oAuth2ClientIds); - Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - Validator.validateId(domainId, id -> INCORRECT_DOMAIN_ID + id); - Validator.checkNotNull(oAuth2ClientIds, "Incorrect oAuth2ClientIds " + oAuth2ClientIds); - if (!oAuth2ClientIds.isEmpty()) { - validateIds(oAuth2ClientIds, ids -> "Incorrect oAuth2ClientIds " + ids); - } - List oauth2Clients = new ArrayList<>(); - for (OAuth2ClientId oAuth2ClientId : oAuth2ClientIds) { - oauth2Clients.add(new DomainOauth2Client(domainId, oAuth2ClientId)); - } + Set newClientList = oAuth2ClientIds.stream() + .map(clientId -> new DomainOauth2Client(domainId, clientId)) + .collect(Collectors.toSet()); + List existingClients = domainDao.findOauth2ClientsByDomainId(tenantId, domainId); - List toRemove = existingClients.stream() - .map(DomainOauth2Client::getOAuth2ClientId) - .filter(clientId -> oAuth2ClientIds.stream().noneMatch(oauth2ClientId -> - oauth2ClientId.equals(clientId))).toList(); - for (OAuth2ClientId clientId : toRemove) { - domainDao.removeOauth2Clients(domainId, clientId); + List toRemoveList = existingClients.stream() + .filter(client -> !newClientList.contains(client)) + .toList(); + newClientList.removeIf(existingClients::contains); + + for (DomainOauth2Client client : toRemoveList) { + domainDao.removeOauth2Client(client); } - for (DomainOauth2Client domainOauth2Client : oauth2Clients) { - domainDao.saveOauth2Clients(domainOauth2Client); + for (DomainOauth2Client client : newClientList) { + domainDao.addOauth2Client(client); } eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) .entityId(domainId).created(false).build()); @@ -121,26 +108,15 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe @Override public PageData findDomainInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDomainInfosByTenantId [{}]", tenantId); - PageData pageData = domainDao.findByTenantId(tenantId, pageLink); - List domainInfos = new ArrayList<>(); - for (Domain domain : pageData.getData()) { - domainInfos.add(new DomainInfo(domain, oauth2ClientDao.findByDomainId(domain.getUuidId()).stream() - .map(OAuth2ClientInfo::new) - .collect(Collectors.toList()))); - } - return new PageData<>(domainInfos, pageData.getTotalPages(), pageData.getTotalElements(), pageData.hasNext()); + PageData domains = domainDao.findByTenantId(tenantId, pageLink); + return domains.mapData(this::getDomainInfo); } @Override public DomainInfo findDomainInfoById(TenantId tenantId, DomainId domainId) { log.trace("Executing findDomainInfoById [{}] [{}]", tenantId, domainId); Domain domain = domainDao.findById(tenantId, domainId.getId()); - if (domain == null) { - return null; - } - return new DomainInfo(domain, oauth2ClientDao.findByDomainId(domain.getUuidId()).stream() - .map(OAuth2ClientInfo::new) - .collect(Collectors.toList())); + return getDomainInfo(domain); } @Override @@ -152,7 +128,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe @Override public void deleteDomainsByTenantId(TenantId tenantId) { log.trace("Executing deleteDomainsByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); domainDao.deleteByTenantId(tenantId); } @@ -166,18 +142,24 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe return Optional.ofNullable(findDomainById(tenantId, new DomainId(entityId.getId()))); } - @Override - public EntityType getEntityType() { - return EntityType.DOMAIN; - } - @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - Domain domain = domainDao.findById(tenantId, id.getId()); + deleteDomainById(tenantId, (DomainId) id); + } + + private DomainInfo getDomainInfo(Domain domain) { if (domain == null) { - return; + return null; } - deleteDomainById(tenantId, domain.getId()); + List clients = oauth2ClientDao.findByDomainId(domain.getUuidId()).stream() + .map(OAuth2ClientInfo::new) + .collect(Collectors.toList()); + return new DomainInfo(domain, clients); + } + + @Override + public EntityType getEntityType() { + return EntityType.DOMAIN; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java index f42acb1638..8da92e1eb3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java @@ -32,9 +32,9 @@ public interface MobileAppDao extends Dao { List findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId); - void saveOauth2Clients(MobileAppOauth2Client mobileAppOauth2Client); + void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client); - void removeOauth2Clients(MobileAppId mobileAppId, OAuth2ClientId oAuth2ClientId); + void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client); void deleteByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index aeac676903..3089cc9ef4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -39,20 +39,16 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; import org.thingsboard.server.dao.service.Validator; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; -import static org.thingsboard.server.dao.service.Validator.validateIds; - @Slf4j @Service public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_MOBILE_APP_ID = "Incorrect mobileApppId "; @Autowired private OAuth2ClientDao oauth2ClientDao; @@ -92,14 +88,8 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil @Override public PageData findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId); - PageData pageData = mobileAppDao.findByTenantId(tenantId, pageLink); - List mobileAppInfos = new ArrayList<>(); - for (MobileApp mobileApp : pageData.getData()) { - mobileAppInfos.add(new MobileAppInfo(mobileApp, oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream() - .map(OAuth2ClientInfo::new) - .collect(Collectors.toList()))); - } - return new PageData<>(mobileAppInfos, pageData.getTotalPages(), pageData.getTotalElements(), pageData.hasNext()); + PageData mobiles = mobileAppDao.findByTenantId(tenantId, pageLink); + return mobiles.mapData(this::getMobileAppInfo); } @Override @@ -109,34 +99,27 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil if (mobileApp == null) { return null; } - return new MobileAppInfo(mobileApp, oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream() - .map(OAuth2ClientInfo::new) - .collect(Collectors.toList())); + return getMobileAppInfo(mobileApp); } @Override public void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List oAuth2ClientIds) { log.trace("Executing updateOauth2Clients, mobileAppId [{}], oAuth2ClientIds [{}]", mobileAppId, oAuth2ClientIds); - Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - Validator.validateId(mobileAppId, id -> INCORRECT_MOBILE_APP_ID + id); - Validator.checkNotNull(oAuth2ClientIds, "Incorrect oAuth2ClientIds " + oAuth2ClientIds); - if (!oAuth2ClientIds.isEmpty()) { - validateIds(oAuth2ClientIds, ids -> "Incorrect oAuth2ClientIds " + ids); - } - List oauth2Clients = new ArrayList<>(); - for (OAuth2ClientId oAuth2ClientId : oAuth2ClientIds) { - oauth2Clients.add(new MobileAppOauth2Client(mobileAppId, oAuth2ClientId)); - } + Set newClientList = oAuth2ClientIds.stream() + .map(clientId -> new MobileAppOauth2Client(mobileAppId, clientId)) + .collect(Collectors.toSet()); + List existingClients = mobileAppDao.findOauth2ClientsByMobileAppId(tenantId, mobileAppId); - List toRemove = existingClients.stream() - .map(MobileAppOauth2Client::getOAuth2ClientId) - .filter(clientId -> oAuth2ClientIds.stream().noneMatch(oauth2ClientId -> - oauth2ClientId.equals(clientId))).toList(); - for (OAuth2ClientId clientId : toRemove) { - mobileAppDao.removeOauth2Clients(mobileAppId, clientId); + List toRemoveList = existingClients.stream() + .filter(client -> !newClientList.contains(client)) + .toList(); + newClientList.removeIf(existingClients::contains); + + for (MobileAppOauth2Client client : toRemoveList) { + mobileAppDao.removeOauth2Client(client); } - for (MobileAppOauth2Client mobileAppOauth2Client : oauth2Clients) { - mobileAppDao.saveOauth2Clients(mobileAppOauth2Client); + for (MobileAppOauth2Client client : newClientList) { + mobileAppDao.addOauth2Client(client); } eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) .entityId(mobileAppId).created(false).build()); @@ -147,19 +130,10 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil return Optional.ofNullable(findMobileAppById(tenantId, new MobileAppId(entityId.getId()))); } - @Override - public EntityType getEntityType() { - return EntityType.MOBILE_APP; - } - @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - MobileApp mobileApp = mobileAppDao.findById(tenantId, id.getId()); - if (mobileApp == null) { - return; - } - deleteMobileAppById(tenantId, mobileApp.getId()); + deleteMobileAppById(tenantId, (MobileAppId) id); } @Override @@ -173,4 +147,16 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil public void deleteByTenantId(TenantId tenantId) { deleteMobileAppsByTenantId(tenantId); } + + private MobileAppInfo getMobileAppInfo(MobileApp mobileApp) { + List clients = oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream() + .map(OAuth2ClientInfo::new) + .collect(Collectors.toList()); + return new MobileAppInfo(mobileApp, clients); + } + + @Override + public EntityType getEntityType() { + return EntityType.MOBILE_APP; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 96c3a9a562..233797285a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -427,7 +427,7 @@ public class ModelConstants { * Domain constants. */ public static final String DOMAIN_TABLE_NAME = "domain"; - public static final String DOMAIN_DOMAIN_NAME_PROPERTY = "domain_name"; + public static final String DOMAIN_NAME_PROPERTY = "name"; public static final String DOMAIN_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled"; public static final String DOMAIN_PROPAGATE_TO_EDGE_PROPERTY = "edge_enabled"; @@ -449,7 +449,7 @@ public class ModelConstants { /** - * OAuth2 client registration constants. + * OAuth2 client constants. */ public static final String OAUTH2_CLIENT_TABLE_NAME = "oauth2_client"; public static final String OAUTH2_CLIENT_REGISTRATION_TEMPLATE_TABLE_NAME = "oauth2_client_registration_template"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java index 6138e8f114..069481a205 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java @@ -39,7 +39,7 @@ public class DomainEntity extends BaseSqlEntity { @Column(name = TENANT_ID_COLUMN) private UUID tenantId; - @Column(name = ModelConstants.DOMAIN_DOMAIN_NAME_PROPERTY) + @Column(name = ModelConstants.DOMAIN_NAME_PROPERTY) private String name; @Column(name = ModelConstants.DOMAIN_OAUTH2_ENABLED_PROPERTY) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java index c4c3b2e2ec..5880db100f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java @@ -36,11 +36,11 @@ public interface OAuth2ClientDao extends Dao { List findByDomainId(UUID domainId); - List findByMobileAppId(UUID domainId); + List findByMobileAppId(UUID mobileAppId); String findAppSecret(UUID id, String pkgName); - void deleteByTenantId(TenantId tenantId); + void deleteByTenantId(UUID tenantId); List findByIds(TenantId tenantId, List oAuth2ClientIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index f7eeb20277..6273ee2763 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -101,11 +101,11 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA } @Override - public String findAppSecret(UUID id, String pkgName) { - log.trace("Executing findAppSecret [{}][{}]", id, pkgName); - validateId(id, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); + public String findAppSecret(OAuth2ClientId oAuth2ClientId, String pkgName) { + log.trace("Executing findAppSecret [{}][{}]", oAuth2ClientId, pkgName); + validateId(oAuth2ClientId, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); validateString(pkgName, "Incorrect package name"); - return oauth2ClientDao.findAppSecret(id, pkgName); + return oauth2ClientDao.findAppSecret(oAuth2ClientId.getId(), pkgName); } @Override @@ -124,19 +124,14 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA public void deleteOauth2ClientsByTenantId(TenantId tenantId) { log.trace("Executing deleteOauth2ClientsByTenantId, tenantId [{}]", tenantId); Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - oauth2ClientDao.deleteByTenantId(tenantId); + oauth2ClientDao.deleteByTenantId(tenantId.getId()); } @Override public PageData findOAuth2ClientInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findOAuth2ClientInfosByTenantId tenantId=[{}]", tenantId); - PageData clientInfos = oauth2ClientDao.findByTenantId(tenantId.getId(), pageLink); - List oAuth2ClientInfos = clientInfos - .getData() - .stream() - .map(OAuth2ClientInfo::new) - .collect(Collectors.toList()); - return new PageData<>(oAuth2ClientInfos, clientInfos.getTotalPages(), clientInfos.getTotalPages(), clientInfos.hasNext()); + PageData clients = oauth2ClientDao.findByTenantId(tenantId.getId(), pageLink); + return clients.mapData(OAuth2ClientInfo::new); } @Override @@ -163,11 +158,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - OAuth2Client oAuth2Client = oauth2ClientDao.findById(tenantId, id.getId()); - if (oAuth2Client == null) { - return; - } - deleteOAuth2ClientById(tenantId, oAuth2Client.getId()); + deleteOAuth2ClientById(tenantId, (OAuth2ClientId) id); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java index 3fe20226d1..40e9d2166f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.sql.domain; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.DomainOauth2ClientCompositeKey; import org.thingsboard.server.dao.model.sql.DomainOauth2ClientEntity; @@ -25,6 +24,6 @@ import java.util.UUID; public interface DomainOauth2ClientRepository extends JpaRepository { - List findAllByDomainId(@Param("domainId") UUID domainId); + List findAllByDomainId(UUID domainId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java index d2cc957fe8..bf3ad8f9cf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java @@ -39,6 +39,6 @@ public interface DomainRepository extends JpaRepository { @Query("DELETE FROM DomainEntity r WHERE r.tenantId = :tenantId") void deleteByTenantId(@Param("tenantId") UUID tenantId); - int countByTenantIdAndOauth2Enabled(@Param("tenantId") UUID tenantId, @Param("oauth2Enabled") boolean oauth2Enabled); + int countByTenantIdAndOauth2Enabled(UUID tenantId, boolean oauth2Enabled); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java index 7701483644..2cbe109af5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java @@ -71,13 +71,14 @@ public class JpaDomainDao extends JpaAbstractDao implement } @Override - public void saveOauth2Clients(DomainOauth2Client domainOauth2Client) { + public void addOauth2Client(DomainOauth2Client domainOauth2Client) { domainOauth2ClientRepository.save(new DomainOauth2ClientEntity(domainOauth2Client)); } @Override - public void removeOauth2Clients(DomainId domainId, OAuth2ClientId oAuth2ClientId) { - domainOauth2ClientRepository.deleteById(new DomainOauth2ClientCompositeKey(domainId.getId(), oAuth2ClientId.getId())); + public void removeOauth2Client(DomainOauth2Client domainOauth2Client) { + domainOauth2ClientRepository.deleteById(new DomainOauth2ClientCompositeKey(domainOauth2Client.getDomainId().getId(), + domainOauth2Client.getOAuth2ClientId().getId())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java index 8f09c7267b..0453481d08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -20,7 +20,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; @@ -62,17 +61,18 @@ public class JpaMobileAppDao extends JpaAbstractDao @Override public List findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId) { - return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppId(mobileAppId.getId())); + return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppId(mobileAppId.getId())); } @Override - public void saveOauth2Clients(MobileAppOauth2Client mobileAppOauth2Client) { + public void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) { mobileOauth2ProviderRepository.save(new MobileAppOauth2ClientEntity(mobileAppOauth2Client)); } @Override - public void removeOauth2Clients(MobileAppId mobileAppId, OAuth2ClientId oAuth2ClientId) { - mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppId.getId(), oAuth2ClientId.getId())); + public void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) { + mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppOauth2Client.getMobileAppId().getId(), + mobileAppOauth2Client.getOAuth2ClientId().getId())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java index 6ce4c99977..9abf9b3b61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.sql.mobile; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey; import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientEntity; @@ -25,6 +24,6 @@ import java.util.UUID; public interface MobileAppOauth2ClientRepository extends JpaRepository { - List findAllByMobileAppId(@Param("mobileAppId") UUID mobileAppId); + List findAllByMobileAppId(UUID mobileAppId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java index d58b68ed7c..cd36da43cf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java @@ -66,7 +66,7 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao findEnabledByPckNameAndPlatformType(String pkgName, PlatformType platformType) { return DaoUtil.convertDataList(repository.findEnabledByPkgNameAndPlatformType(pkgName, - platformType != null ? "%" + platformType.name() + "%" : null)); + platformType != null ? platformType.name() : null)); } @Override @@ -85,8 +85,8 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao findEnabledByDomainNameAndPlatformType(@Param("domainName") String domainName, @Param("platformFilter") String platformFilter); @@ -48,8 +48,8 @@ public interface OAuth2ClientRepository extends JpaRepository findEnabledByPkgNameAndPlatformType(@Param("pkgName") String pkgName, @Param("platformFilter") String platformFilter); @@ -79,6 +79,6 @@ public interface OAuth2ClientRepository extends JpaRepository findByTenantIdAndIdIn(UUID tenantId, List queueStatsIds); + List findByTenantIdAndIdIn(UUID tenantId, List uuids); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 75d04757ad..8e6c38814a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -177,6 +177,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Fri, 16 Aug 2024 18:08:38 +0300 Subject: [PATCH 224/331] UI: Fix show widget action in tooltip --- .../components/widget/widget-container.component.ts | 12 +++++++----- .../modules/home/models/widget-component.models.ts | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 698b78eaa7..b45b3432c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -148,7 +148,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); $(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent)); - this.initEditWidgetActionTooltip(); + const dashboardElement = this.widget.widgetContext.dashboardPageElement; + if (dashboardElement) { + this.initEditWidgetActionTooltip(dashboardElement); + } } ngAfterViewInit(): void { @@ -251,11 +254,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O } } - private initEditWidgetActionTooltip() { + private initEditWidgetActionTooltip(parent: HTMLElement) { from(import('tooltipster')).subscribe(() => { - const dashboardElement = this.widget.widgetContext.dashboard.stateController.dashboardCtrl.elRef.nativeElement; $(this.gridsterItem.el).tooltipster({ - parent: $(dashboardElement), + parent: $(parent), delay: this.widget.selected ? [0, 10000000] : [0, 100], distance: 2, zIndex: 151, @@ -275,7 +277,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O content: '', functionPosition: (instance, helper, position) => { const clientRect = helper.origin.getBoundingClientRect(); - const container = dashboardElement.getBoundingClientRect(); + const container = parent.getBoundingClientRect(); position.coord.left = clientRect.right - position.size.width - container.left; position.coord.top = position.coord.top - container.top; position.target = clientRect.right; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 1441bd4e92..bad8615363 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -180,6 +180,10 @@ export class WidgetContext { } } + get dashboardPageElement(): HTMLElement { + return this.dashboard?.stateController?.dashboardCtrl?.elRef?.nativeElement; + } + authService: AuthService; deviceService: DeviceService; assetService: AssetService; From 4ec74a73bea085d7aca7f43992b325b1cb1f4ae0 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 16 Aug 2024 18:30:16 +0300 Subject: [PATCH 225/331] UI: add SCADA tank --- .../system/scada_symbols/cylindrical-tank.svg | 1401 ++++++++++++++ .../system/scada_symbols/elevated-tank.svg | 1633 +++++++++++++++++ .../system/scada_symbols/horizontal-tank.svg | 1407 ++++++++++++++ .../scada_symbols/large-cylindrical-tank.svg | 1392 ++++++++++++++ .../large-stand-cylindrical-tank.svg | 1453 +++++++++++++++ .../large-stand-vertical-tank.svg | 1455 +++++++++++++++ .../scada_symbols/large-vertical-tank.svg | 1394 ++++++++++++++ .../data/json/system/scada_symbols/pool.svg | 1056 +++++++++++ .../scada_symbols/small-spherical-tank.svg | 1426 ++++++++++++++ .../system/scada_symbols/spherical-tank.svg | 1456 +++++++++++++++ .../scada_symbols/stand-cylindrical-tank.svg | 1462 +++++++++++++++ .../scada_symbols/stand-horizontal-tank.svg | 1468 +++++++++++++++ .../stand-vertical-short-tank.svg | 1435 +++++++++++++++ .../scada_symbols/stand-vertical-tank.svg | 1455 +++++++++++++++ .../scada_symbols/vertical-short-tank.svg | 1374 ++++++++++++++ .../system/scada_symbols/vertical-tank.svg | 4 +- ...cada-symbol-object-settings.component.html | 2 +- 17 files changed, 21270 insertions(+), 3 deletions(-) create mode 100644 application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/elevated-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/horizontal-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/pool.svg create mode 100644 application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/spherical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg create mode 100644 application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg new file mode 100644 index 0000000000..293de230cc --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -0,0 +1,1401 @@ + +{ + "title": "Cylindrical tank", + "description": "Cylindrical tank with current volume value and level visualizations.", + "searchTags": [ + "tank" + ], + "widgetSizeX": 3, + "widgetSizeY": 5, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "##F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg new file mode 100644 index 0000000000..120604ae95 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -0,0 +1,1633 @@ + +{ + "title": "Elevated tank", + "description": "Elevated tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "elevator" + ], + "widgetSizeX": 6, + "widgetSizeY": 10, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*895; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*895; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "transparent", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg new file mode 100644 index 0000000000..c504f0303b --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -0,0 +1,1407 @@ + +{ + "title": "Horizontal tank", + "description": "Horizontal tank with current volume value and level visualizations.", + "searchTags": [ + "tank" + ], + "widgetSizeX": 5, + "widgetSizeY": 3, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg new file mode 100644 index 0000000000..4773147dff --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -0,0 +1,1392 @@ + +{ + "title": "Large cylindrical tank", + "description": " Large cylindrical tank with current volume value and level visualizations.", + "searchTags": [ + "tank" + ], + "widgetSizeX": 5, + "widgetSizeY": 5, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg new file mode 100644 index 0000000000..ef365ab465 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -0,0 +1,1453 @@ + +{ + "title": "Large stand cylindrical tank", + "description": "Large stand cylindrical tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tank" + ], + "widgetSizeX": 5, + "widgetSizeY": 6, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg new file mode 100644 index 0000000000..d38164a814 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -0,0 +1,1455 @@ + +{ + "title": "Large stand vertical tank", + "description": "Large stand tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tand" + ], + "widgetSizeX": 5, + "widgetSizeY": 6, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg new file mode 100644 index 0000000000..0387e160ad --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -0,0 +1,1394 @@ + +{ + "title": "Large vertical tank", + "description": "Large vertical tank with current volume value and level visualizations.", + "searchTags": [ + "tank" + ], + "widgetSizeX": 5, + "widgetSizeY": 5, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/pool.svg b/application/src/main/data/json/system/scada_symbols/pool.svg new file mode 100644 index 0000000000..a0eab2be0b --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/pool.svg @@ -0,0 +1,1056 @@ + +{ + "title": "Pool", + "description": "Pool with current volume value and level visualizations.", + "searchTags": [ + "pool" + ], + "widgetSizeX": 12, + "widgetSizeY": 4, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*740; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*740; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg new file mode 100644 index 0000000000..b42fd2ab5b --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -0,0 +1,1426 @@ + +{ + "title": "Small spherical tank", + "description": "Small spherical tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tank" + ], + "widgetSizeX": 3, + "widgetSizeY": 3, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*560; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*560; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg new file mode 100644 index 0000000000..cf11b61bba --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -0,0 +1,1456 @@ + +{ + "title": "Spherical tank", + "description": "Spherical tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tank" + ], + "widgetSizeX": 5, + "widgetSizeY": 5, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*960; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*960; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg new file mode 100644 index 0000000000..f9948bc64b --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -0,0 +1,1462 @@ + +{ + "title": "Stand cylindrical tank", + "description": "Stand cylindrical tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tank" + ], + "widgetSizeX": 3, + "widgetSizeY": 6, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg new file mode 100644 index 0000000000..9adfa3fb82 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -0,0 +1,1468 @@ + +{ + "title": "Stand horizontal tank", + "description": "Stand horizontal tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tank" + ], + "widgetSizeX": 5, + "widgetSizeY": 4, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg new file mode 100644 index 0000000000..5c181e46d9 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -0,0 +1,1435 @@ + +{ + "title": "Stand vertical short tank", + "description": "Stand vertical short tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "short tank", + "stand tank" + ], + "widgetSizeX": 4, + "widgetSizeY": 4, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 245});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 245 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg new file mode 100644 index 0000000000..e7d14a9f23 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -0,0 +1,1455 @@ + +{ + "title": "Stand vertical tank", + "description": "Stand vertical tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "stand tank" + ], + "widgetSizeX": 3, + "widgetSizeY": 6, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg new file mode 100644 index 0000000000..662bdd1bb2 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -0,0 +1,1374 @@ + +{ + "title": "Vertical short tank", + "description": "Vertical short tank with current volume value and level visualizations.", + "searchTags": [ + "tank", + "short tank" + ], + "widgetSizeX": 4, + "widgetSizeY": 3, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 245});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 245 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#F3F3F3", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index 57d4d1978a..258cb590d0 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -311,7 +311,7 @@ "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", - "default": "#FFFFFF", + "default": "#F3F3F3", "required": null, "subLabel": null, "divider": null, @@ -633,7 +633,7 @@ - + 1660 gal diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html index 53d021ccf4..5b6d50bf1c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html @@ -40,7 +40,7 @@
widget-config.appearance
- + {{ propertyRow.label | customTranslate }}
{{ propertyRow.label | customTranslate }}
From 6f307cdede48b4aa5d01db8a47033a7372867b8a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 19 Aug 2024 11:27:20 +0300 Subject: [PATCH 226/331] SNMP v3: fix engine id --- .../server/transport/snmp/service/SnmpAuthService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java index aca736c3ed..bcc3240570 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java @@ -69,7 +69,7 @@ public class SnmpAuthService { case V3: OctetString username = new OctetString(deviceTransportConfig.getUsername()); OctetString securityName = new OctetString(deviceTransportConfig.getSecurityName()); - OctetString engineId = new OctetString(deviceTransportConfig.getEngineId()); + OctetString engineId = OctetString.fromString(deviceTransportConfig.getEngineId(), 16); OID authenticationProtocol = new OID(deviceTransportConfig.getAuthenticationProtocol().getOid()); byte[] authenticationPassphrase = SecurityProtocols.getInstance().passwordToKey(authenticationProtocol, From 006e12fd09b7c1872c19b07cc92eb1bda9c95b05 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 19 Aug 2024 12:14:15 +0300 Subject: [PATCH 227/331] changed scan period back to milliseconds --- .../opc-server-config.component.html | 12 ++++++------ .../opc-server-config/opc-server-config.component.ts | 7 ++++--- .../widget/lib/gateway/gateway-widget.models.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 8 ++++---- .../metadata/connector-default-configs/opcua.json | 3 ++- .../connector-default-configs/opcua_asyncio.json | 2 +- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html index 057a4d1882..584679fa2c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html @@ -69,15 +69,15 @@
- + warning diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 46e7fd0c9c..083d79ccb7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -39,6 +39,7 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; +import { HOUR } from '@shared/models/time/time.models'; @Component({ selector: 'tb-opc-server-config', @@ -80,7 +81,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator name: ['', []], url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]], - scanPeriodInSec: [3600, [Validators.required, Validators.min(1)]], + scanPeriodInMillis: [HOUR, [Validators.required, Validators.min(1000)]], pollPeriodInMillis: [5000, [Validators.required, Validators.min(50)]], enableSubscriptions: [true, []], subCheckPeriodInMillis: [100, [Validators.required, Validators.min(100)]], @@ -119,7 +120,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator writeValue(serverConfig: ServerConfig): void { const { timeoutInMillis = 1000, - scanPeriodInSec = 3600, + scanPeriodInMillis = HOUR, pollPeriodInMillis = 5000, enableSubscriptions = true, subCheckPeriodInMillis = 100, @@ -131,7 +132,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator this.serverConfigFormGroup.reset({ ...serverConfig, timeoutInMillis, - scanPeriodInSec, + scanPeriodInMillis, pollPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 0ad4975a4b..2dde0b8f54 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -150,7 +150,7 @@ export interface ServerConfig { name: string; url: string; timeoutInMillis: number; - scanPeriodInSec: number; + scanPeriodInMillis: number; pollPeriodInMillis: number; enableSubscriptions: boolean; subCheckPeriodInMillis: number; 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 ba4f031d14..6eeaad774e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3178,8 +3178,8 @@ "without-response": "Without response", "other": "Other", "save-tip": "Save configuration file", - "scan-period": "Scan period (s)", - "scan-period-error": "Scan period should be at least {{min}} (s).", + "scan-period": "Scan period (ms)", + "scan-period-error": "Scan period should be at least {{min}} (ms).", "sub-check-period": "Subscription check period (ms)", "sub-check-period-error": "Subscription check period should be at least {{min}} (ms).", "security": "Security", @@ -3389,9 +3389,9 @@ "file": "Your data will be stored in separated files and will be saved even after the gateway restart.", "sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.", "opc-timeout": "Timeout in milliseconds for connecting to OPC-UA server.", - "scan-period": "Period in seconds to rescan the server.", + "scan-period": "Period in milliseconds to rescan the server.", "sub-check-period": "Period to check the subscriptions in the OPC-UA server.", - "enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInSec.", + "enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.", "show-map": "Show nodes on scanning.", "method-name": "Name of method on OPC-UA server.", "arguments": "Arguments for the method (will be overwritten by arguments from the RPC request).", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json index 97621deda8..e9351b23f6 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json @@ -3,7 +3,8 @@ "name": "OPC-UA Default Server", "url": "localhost:4840/freeopcua/server/", "timeoutInMillis": 5000, - "scanPeriodInMillis": 5000, + "scanPeriodInMillis": 3600000, + "pollPeriodInMillis": 5000, "enableSubscriptions": true, "subCheckPeriodInMillis": 100, "showMap": false, diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json index 453f23735d..65fb6c5145 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json @@ -3,7 +3,7 @@ "name": "OPC-UA Default Server", "url": "localhost:4840/freeopcua/server/", "timeoutInMillis": 5000, - "scanPeriodInSec": 3600, + "scanPeriodInMillis": 3600000, "pollPeriodInMillis": 5000, "disableSubscriptions": false, "subCheckPeriodInMillis": 100, From 78b43c04aae03cf34187f4df40bc7e2fa2b42707 Mon Sep 17 00:00:00 2001 From: rusikv Date: Mon, 19 Aug 2024 13:47:49 +0300 Subject: [PATCH 228/331] UI: fixed client form blocking save, mobile app enabled by default --- .../src/app/modules/home/pages/admin/admin-routing.module.ts | 4 ++-- .../home/pages/admin/oauth2/clients/client.component.ts | 1 + .../pages/admin/oauth2/mobile-apps/mobile-app.component.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 0b6ed5a2be..c1d32a44c8 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -15,8 +15,8 @@ /// import { inject, NgModule } from '@angular/core'; -import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; -import { RouterModule, Routes } from '@angular/router'; +import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; + import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { Authority } from '@shared/models/authority.enum'; diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts index 90f0b17403..cf60b3ed51 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts @@ -278,6 +278,7 @@ export class ClientComponent extends EntityComponent { pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required]], appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), [Validators.required, this.base64Format]], - oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : false, + oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true, oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos.map(info => info.id.id) : [] }); } From 938693bb35aa152e2b0264918e5fb2e61fca8aa2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 19 Aug 2024 14:44:32 +0300 Subject: [PATCH 229/331] UI: Fixed delete widget in layout and improved dashboard layout types --- .../core/services/dashboard-utils.service.ts | 23 +++++++------------ .../app/core/services/item-buffer.service.ts | 10 ++++---- ...nage-dashboard-layouts-dialog.component.ts | 8 +++---- .../select-dashboard-breakpoint.component.ts | 4 ++-- .../import-export/import-export.service.ts | 4 ++-- .../src/app/shared/models/dashboard.models.ts | 2 +- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index c3cead0bc2..8d599ad925 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -698,16 +698,9 @@ export class DashboardUtilsService { targetState: string, targetLayout: DashboardLayoutId, widgetId: string, - breakpoint: string) { - const dashboardConfiguration = dashboard.configuration; - const states = dashboardConfiguration.states; - const state = states[targetState]; - const layout = state.layouts[targetLayout]; - if (layout.breakpoints[breakpoint]) { - delete layout.breakpoints[breakpoint].widgets[widgetId]; - } else { - delete layout.widgets[widgetId]; - } + breakpoint: BreakpointId) { + const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[targetState].layouts[targetLayout], breakpoint); + delete layout.widgets[widgetId]; this.removeUnusedWidgets(dashboard); } @@ -948,7 +941,7 @@ export class DashboardUtilsService { dashboard: Dashboard, targetState: string, targetLayout: DashboardLayoutId, - breakpointId: string, + breakpointId: BreakpointId, isRemoveWidget: boolean): Widget { const newWidget = deepClone(widget); @@ -972,14 +965,14 @@ export class DashboardUtilsService { return newWidget; } - getDashboardLayoutConfig(layout: DashboardLayout, breakpointId: string): DashboardLayout { - if (breakpointId !== 'default') { + getDashboardLayoutConfig(layout: DashboardLayout, breakpointId: BreakpointId): DashboardLayout { + if (breakpointId !== 'default' && layout.breakpoints) { return layout.breakpoints[breakpointId]; } return layout; } - getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, breakpointId: string): number { + getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, breakpointId: BreakpointId): number { let originalColumns = 24; let gridSettings = null; const state = dashboard.configuration.states[sourceState]; @@ -998,7 +991,7 @@ export class DashboardUtilsService { } getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, - widget: Widget, breakpointId: string): WidgetSize { + widget: Widget, breakpointId: BreakpointId): WidgetSize { const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[sourceState].layouts[sourceLayout], breakpointId); const widgetLayout = layout.widgets[widget.id]; return { diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index 3aa45c8439..c77784c4f5 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; +import { BreakpointId, Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models'; import { Datasource, @@ -87,7 +87,7 @@ export class ItemBufferService { private utils: UtilsService) {} public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, - widget: Widget, breakpoint: string): WidgetItem { + widget: Widget, breakpoint: BreakpointId): WidgetItem { const aliasesInfo: AliasesInfo = { datasourceAliases: {}, targetDeviceAlias: null @@ -148,13 +148,13 @@ export class ItemBufferService { }; } - public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): void { + public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId) { const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint); this.storeSet(WIDGET_ITEM, widgetItem); } public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, - widget: Widget, breakpoint: string): void { + widget: Widget, breakpoint: BreakpointId): void { const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint); this.storeSet(WIDGET_REFERENCE, widgetReference); } @@ -412,7 +412,7 @@ export class ItemBufferService { } private prepareWidgetReference(dashboard: Dashboard, sourceState: string, - sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): WidgetReference { + sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId): WidgetReference { const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint); const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint); return { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts index 9dd81de7ae..77786e1cae 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts @@ -71,7 +71,7 @@ export interface DashboardLayoutSettings { name: string; descriptionSize?: string; layout: DashboardLayout; - breakpoint: string; + breakpoint: BreakpointId; } @Component({ @@ -103,8 +103,8 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent, protected router: Router, @@ -442,7 +442,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent = ['default']; + breakpointIds: Array = ['default']; private layoutDataChanged$: Subscription; @@ -42,7 +42,7 @@ export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy { ngOnInit() { this.layoutDataChanged$ = this.dashboardCtrl.layouts.main.layoutCtx.layoutDataChanged.subscribe(() => { if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) { - this.breakpointIds = Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData); + this.breakpointIds = Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData) as BreakpointId[]; this.breakpointIds.sort((a, b) => { const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity; const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity; diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 9e2d066400..528a8b0fd3 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -20,7 +20,7 @@ import { TranslateService } from '@ngx-translate/core'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ActionNotificationShow } from '@core/notification/notification.actions'; -import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; +import { BreakpointId, Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils'; import { WINDOW } from '@core/services/window.service'; import { DOCUMENT } from '@angular/common'; @@ -199,7 +199,7 @@ export class ImportExportService { } public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, - widgetTitle: string, breakpoint: string) { + widgetTitle: string, breakpoint: BreakpointId) { const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint); const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName; let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 04606fb612..2621175030 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -104,7 +104,7 @@ export interface DashboardLayout { breakpoints?: {[breakpointId in BreakpointId]?: Omit}; } -export declare type DashboardLayoutInfo = {[breakpointId: string]: BreakpointLayoutInfo}; +export declare type DashboardLayoutInfo = {[breakpointId in BreakpointId]?: BreakpointLayoutInfo}; export interface BreakpointLayoutInfo { widgetIds?: string[]; From 69ff2bb506c907c714a63524851585ae63670d2d Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Mon, 19 Aug 2024 14:51:53 +0300 Subject: [PATCH 230/331] Update entity-list.component.ts --- .../shared/components/entity/entity-list.component.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 7286258c5a..ffa8e915f1 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -56,6 +56,11 @@ import { coerceBoolean } from '@shared/decorators/coercion'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EntityListComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityListComponent), + multi: true } ] }) @@ -203,6 +208,12 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV this.dirty = true; } + validate(): ValidationErrors | null { + return this.entityListFormGroup.valid ? null : { + entities: {valid: false} + }; + } + private reset() { this.entities = []; this.entityListFormGroup.get('entities').setValue(this.entities); From 858bdfeea51473acd763f9c8ecdb7f97031939f7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 19 Aug 2024 15:10:23 +0300 Subject: [PATCH 231/331] fixed double request on discard --- ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts b/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts index f39716dc5e..e34aca7007 100644 --- a/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts @@ -75,7 +75,7 @@ export class EntityConflictInterceptor implements HttpInterceptor { return next.handle(this.updateRequestVersion(request)); } (request.params as HttpParams & { interceptorConfig: InterceptorConfig }).interceptorConfig.ignoreErrors = true; - return next.handle(request); + return throwError(() => new Error(error.error.message)); } return of(null); }) From 94f21cb6a1d76fd8a9c0c8d4431e0a17657014ec Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 19 Aug 2024 15:19:57 +0300 Subject: [PATCH 232/331] UI: Menu refactoring. --- ui-ngx/src/app/core/services/menu.models.ts | 771 +++++++++++++++++- ui-ngx/src/app/core/services/menu.service.ts | 642 +-------------- .../home/pages/admin/admin-routing.module.ts | 52 +- .../home/pages/alarm/alarm-routing.module.ts | 4 +- .../api-usage/api-usage-routing.module.ts | 4 +- .../asset-profile-routing.module.ts | 4 +- .../home/pages/asset/asset-routing.module.ts | 4 +- .../audit-log/audit-log-routing.module.ts | 4 +- .../pages/customer/customer-routing.module.ts | 4 +- .../dashboard/dashboard-routing.module.ts | 4 +- .../device-profile-routing.module.ts | 4 +- .../pages/device/device-routing.module.ts | 4 +- .../home/pages/edge/edge-routing.module.ts | 10 +- .../entity-view/entity-view-routing.module.ts | 4 +- .../pages/features/features-routing.module.ts | 4 +- .../home-links/home-links-routing.module.ts | 4 +- .../notification-routing.module.ts | 19 +- .../ota-update/ota-update-routing.module.ts | 4 +- .../pages/profiles/profiles-routing.module.ts | 4 +- .../rulechain/rulechain-routing.module.ts | 5 +- .../tenant-profile-routing.module.ts | 4 +- .../pages/tenant/tenant-routing.module.ts | 4 +- .../home/pages/vc/vc-routing.module.ts | 4 +- .../widget/widget-library-routing.module.ts | 10 +- .../components/breadcrumb.component.html | 4 +- .../shared/components/breadcrumb.component.ts | 36 +- .../src/app/shared/components/breadcrumb.ts | 6 +- 27 files changed, 889 insertions(+), 734 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index b1278d4db3..703397fee1 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -14,11 +14,14 @@ /// limitations under the License. /// -import { HasUUID } from '@shared/models/id/has-uuid'; +import { AuthState } from '@core/auth/auth.models'; +import { Authority } from '@shared/models/authority.enum'; +import { deepClone } from '@core/utils'; export declare type MenuSectionType = 'link' | 'toggle'; -export interface MenuSection extends HasUUID{ +export interface MenuSection { + id: MenuId | string; name: string; fullName?: string; type: MenuSectionType; @@ -30,6 +33,11 @@ export interface MenuSection extends HasUUID{ rootOnly?: boolean; } +export interface MenuReference { + id: MenuId; + pages?: Array; +} + export interface HomeSection { name: string; places: Array; @@ -40,3 +48,762 @@ export interface HomeSectionPlace { icon: string; path: string; } + +export enum MenuId { + home = 'home', + tenants = 'tenants', + tenant_profiles = 'tenant_profiles', + resources = 'resources', + widget_library = 'widget_library', + widget_types = 'widget_types', + widgets_bundles = 'widgets_bundles', + images = 'images', + scada_symbols = 'scada_symbols', + resources_library = 'resources_library', + notifications_center = 'notifications_center', + notification_inbox = 'notification_inbox', + notification_sent = 'notification_sent', + notification_recipients = 'notification_recipients', + notification_templates = 'notification_templates', + notification_rules = 'notification_rules', + settings = 'settings', + general = 'general', + mail_server = 'mail_server', + home_settings = 'home_settings', + notification_settings = 'notification_settings', + repository_settings = 'repository_settings', + auto_commit_settings = 'auto_commit_settings', + queues = 'queues', + mobile_app_settings = 'mobile_app_settings', + security_settings = 'security_settings', + security_settings_general = 'security_settings_general', + two_fa = '2fa', + oauth2 = 'oauth2', + audit_log = 'audit_log', + alarms = 'alarms', + dashboards = 'dashboards', + entities = 'entities', + devices = 'devices', + assets = 'assets', + entity_views = 'entity_views', + profiles = 'profiles', + device_profiles = 'device_profiles', + asset_profiles = 'asset_profiles', + customers = 'customers', + rule_chains = 'rule_chains', + edge_management = 'edge_management', + edges = 'edges', + rulechain_templates = 'rulechain_templates', + features = 'features', + otaUpdates = 'otaUpdates', + version_control = 'version_control', + api_usage = 'api_usage' +} + +declare type MenuFilter = (authState: AuthState) => boolean; + +const menuSectionMap = new Map([ + [ + MenuId.home, + { + id: MenuId.home, + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + } + ], + [ + MenuId.tenants, + { + id: MenuId.tenants, + name: 'tenant.tenants', + type: 'link', + path: '/tenants', + icon: 'supervisor_account' + } + ], + [ + MenuId.tenant_profiles, + { + id: MenuId.tenant_profiles, + name: 'tenant-profile.tenant-profiles', + type: 'link', + path: '/tenantProfiles', + icon: 'mdi:alpha-t-box' + } + ], + [ + MenuId.resources, + { + id: MenuId.resources, + name: 'admin.resources', + type: 'toggle', + path: '/resources', + icon: 'folder' + } + ], + [ + MenuId.widget_library, + { + id: MenuId.widget_library, + name: 'widget.widget-library', + type: 'link', + path: '/resources/widgets-library', + icon: 'now_widgets' + } + ], + [ + MenuId.widget_types, + { + id: MenuId.widget_types, + name: 'widget.widgets', + type: 'link', + path: '/resources/widgets-library/widget-types', + icon: 'now_widgets' + } + ], + [ + MenuId.widgets_bundles, + { + id: MenuId.widgets_bundles, + name: 'widgets-bundle.widgets-bundles', + type: 'link', + path: '/resources/widgets-library/widgets-bundles', + icon: 'now_widgets' + } + ], + [ + MenuId.images, + { + id: MenuId.images, + name: 'image.gallery', + type: 'link', + path: '/resources/images', + icon: 'filter' + } + ], + [ + MenuId.scada_symbols, + { + id: MenuId.scada_symbols, + name: 'scada.symbols', + type: 'link', + path: '/resources/scada-symbols', + icon: 'view_in_ar' + } + ], + [ + MenuId.resources_library, + { + id: MenuId.resources_library, + name: 'resource.resources-library', + type: 'link', + path: '/resources/resources-library', + icon: 'mdi:rhombus-split' + } + ], + [ + MenuId.notifications_center, + { + id: MenuId.notifications_center, + name: 'notification.notification-center', + type: 'link', + path: '/notification', + icon: 'mdi:message-badge' + } + ], + [ + MenuId.notification_inbox, + { + id: MenuId.notification_inbox, + name: 'notification.inbox', + fullName: 'notification.notification-inbox', + type: 'link', + path: '/notification/inbox', + icon: 'inbox' + } + ], + [ + MenuId.notification_sent, + { + id: MenuId.notification_sent, + name: 'notification.sent', + fullName: 'notification.notification-sent', + type: 'link', + path: '/notification/sent', + icon: 'outbox' + } + ], + [ + MenuId.notification_recipients, + { + id: MenuId.notification_recipients, + name: 'notification.recipients', + fullName: 'notification.notification-recipients', + type: 'link', + path: '/notification/recipients', + icon: 'contacts' + } + ], + [ + MenuId.notification_templates, + { + id: MenuId.notification_templates, + name: 'notification.templates', + fullName: 'notification.notification-templates', + type: 'link', + path: '/notification/templates', + icon: 'mdi:message-draw' + } + ], + [ + MenuId.notification_rules, + { + id: MenuId.notification_rules, + name: 'notification.rules', + fullName: 'notification.notification-rules', + type: 'link', + path: '/notification/rules', + icon: 'mdi:message-cog' + } + ], + [ + MenuId.settings, + { + id: MenuId.settings, + name: 'admin.settings', + type: 'link', + path: '/settings', + icon: 'settings' + } + ], + [ + MenuId.general, + { + id: MenuId.general, + name: 'admin.general', + fullName: 'admin.general-settings', + type: 'link', + path: '/settings/general', + icon: 'settings_applications' + } + ], + [ + MenuId.mail_server, + { + id: MenuId.mail_server, + name: 'admin.outgoing-mail', + type: 'link', + path: '/settings/outgoing-mail', + icon: 'mail' + } + ], + [ + MenuId.home_settings, + { + id: MenuId.home_settings, + name: 'admin.home', + fullName: 'admin.home-settings', + type: 'link', + path: '/settings/home', + icon: 'settings_applications' + } + ], + [ + MenuId.notification_settings, + { + id: MenuId.notification_settings, + name: 'admin.notifications', + fullName: 'admin.notifications-settings', + type: 'link', + path: '/settings/notifications', + icon: 'mdi:message-badge' + } + ], + [ + MenuId.repository_settings, + { + id: MenuId.repository_settings, + name: 'admin.repository', + fullName: 'admin.repository-settings', + type: 'link', + path: '/settings/repository', + icon: 'manage_history' + } + ], + [ + MenuId.auto_commit_settings, + { + id: MenuId.auto_commit_settings, + name: 'admin.auto-commit', + fullName: 'admin.auto-commit-settings', + type: 'link', + path: '/settings/auto-commit', + icon: 'settings_backup_restore' + } + ], + [ + MenuId.queues, + { + id: MenuId.queues, + name: 'admin.queues', + type: 'link', + path: '/settings/queues', + icon: 'swap_calls' + } + ], + [ + MenuId.mobile_app_settings, + { + id: MenuId.mobile_app_settings, + name: 'admin.mobile-app.mobile-app', + fullName: 'admin.mobile-app.mobile-app', + type: 'link', + path: '/settings/mobile-app', + icon: 'smartphone' + } + ], + [ + MenuId.security_settings, + { + id: MenuId.security_settings, + name: 'security.security', + type: 'toggle', + path: '/security-settings', + icon: 'security' + } + ], + [ + MenuId.security_settings_general, + { + id: MenuId.security_settings_general, + name: 'admin.general', + fullName: 'security.general-settings', + type: 'link', + path: '/security-settings/general', + icon: 'settings_applications' + } + ], + [ + MenuId.two_fa, + { + id: MenuId.two_fa, + name: 'admin.2fa.2fa', + type: 'link', + path: '/security-settings/2fa', + icon: 'mdi:two-factor-authentication' + } + ], + [ + MenuId.oauth2, + { + id: MenuId.oauth2, + name: 'admin.oauth2.oauth2', + type: 'link', + path: '/security-settings/oauth2', + icon: 'mdi:shield-account' + } + ], + [ + MenuId.audit_log, + { + id: MenuId.audit_log, + name: 'audit-log.audit-logs', + type: 'link', + path: '/security-settings/auditLogs', + icon: 'track_changes' + } + ], + [ + MenuId.alarms, + { + id: MenuId.alarms, + name: 'alarm.alarms', + type: 'link', + path: '/alarms', + icon: 'mdi:alert-outline' + } + ], + [ + MenuId.dashboards, + { + id: MenuId.dashboards, + name: 'dashboard.dashboards', + type: 'link', + path: '/dashboards', + icon: 'dashboards' + } + ], + [ + MenuId.entities, + { + id: MenuId.entities, + name: 'entity.entities', + type: 'toggle', + path: '/entities', + icon: 'category' + } + ], + [ + MenuId.devices, + { + id: MenuId.devices, + name: 'device.devices', + type: 'link', + path: '/entities/devices', + icon: 'devices_other' + } + ], + [ + MenuId.assets, + { + id: MenuId.assets, + name: 'asset.assets', + type: 'link', + path: '/entities/assets', + icon: 'domain' + } + ], + [ + MenuId.entity_views, + { + id: MenuId.entity_views, + name: 'entity-view.entity-views', + type: 'link', + path: '/entities/entityViews', + icon: 'view_quilt' + } + ], + [ + MenuId.profiles, + { + id: MenuId.profiles, + name: 'profiles.profiles', + type: 'toggle', + path: '/profiles', + icon: 'badge' + } + ], + [ + MenuId.device_profiles, + { + id: MenuId.device_profiles, + name: 'device-profile.device-profiles', + type: 'link', + path: '/profiles/deviceProfiles', + icon: 'mdi:alpha-d-box' + } + ], + [ + MenuId.asset_profiles, + { + id: MenuId.asset_profiles, + name: 'asset-profile.asset-profiles', + type: 'link', + path: '/profiles/assetProfiles', + icon: 'mdi:alpha-a-box' + } + ], + [ + MenuId.customers, + { + id: MenuId.customers, + name: 'customer.customers', + type: 'link', + path: '/customers', + icon: 'supervisor_account' + } + ], + [ + MenuId.rule_chains, + { + id: MenuId.rule_chains, + name: 'rulechain.rulechains', + type: 'link', + path: '/ruleChains', + icon: 'settings_ethernet' + } + ], + [ + MenuId.edge_management, + { + id: MenuId.edge_management, + name: 'edge.management', + type: 'toggle', + path: '/edgeManagement', + icon: 'settings_input_antenna' + } + ], + [ + MenuId.edges, + { + id: MenuId.edges, + name: 'edge.instances', + fullName: 'edge.edge-instances', + type: 'link', + path: '/edgeManagement/instances', + icon: 'router' + } + ], + [ + MenuId.rulechain_templates, + { + id: MenuId.rulechain_templates, + name: 'edge.rulechain-templates', + fullName: 'edge.edge-rulechain-templates', + type: 'link', + path: '/edgeManagement/ruleChains', + icon: 'settings_ethernet' + } + ], + [ + MenuId.features, + { + id: MenuId.features, + name: 'feature.advanced-features', + type: 'toggle', + path: '/features', + icon: 'construction' + } + ], + [ + MenuId.otaUpdates, + { + id: MenuId.otaUpdates, + name: 'ota-update.ota-updates', + type: 'link', + path: '/features/otaUpdates', + icon: 'memory' + } + ], + [ + MenuId.version_control, + { + id: MenuId.version_control, + name: 'version-control.version-control', + type: 'link', + path: '/features/vc', + icon: 'history' + } + ], + [ + MenuId.api_usage, + { + id: MenuId.api_usage, + name: 'api-usage.api-usage', + type: 'link', + path: '/usage', + icon: 'insert_chart' + } + ] +]); + +const menuFilters = new Map([ + [ + MenuId.edges, (authState) => authState.edgesSupportEnabled + ], + [ + MenuId.edge_management, (authState) => authState.edgesSupportEnabled + ], + [ + MenuId.rulechain_templates, (authState) => authState.edgesSupportEnabled + ] +]); + +const defaultUserMenuMap = new Map([ + [ + Authority.SYS_ADMIN, + [ + {id: MenuId.home}, + {id: MenuId.tenants}, + {id: MenuId.tenant_profiles}, + { + id: MenuId.resources, + pages: [ + { + id: MenuId.widget_library, + pages: [ + {id: MenuId.widget_types}, + {id: MenuId.widgets_bundles} + ] + }, + {id: MenuId.images}, + {id: MenuId.scada_symbols}, + {id: MenuId.resources_library} + ] + }, + { + id: MenuId.notifications_center, + pages: [ + {id: MenuId.notification_inbox}, + {id: MenuId.notification_sent}, + {id: MenuId.notification_recipients}, + {id: MenuId.notification_templates}, + {id: MenuId.notification_rules} + ] + }, + { + id: MenuId.settings, + pages: [ + {id: MenuId.general}, + {id: MenuId.mail_server}, + {id: MenuId.notification_settings}, + {id: MenuId.queues}, + {id: MenuId.mobile_app_settings} + ] + }, + { + id: MenuId.security_settings, + pages: [ + {id: MenuId.security_settings_general}, + {id: MenuId.two_fa}, + {id: MenuId.oauth2} + ] + } + ] + ], + [ + Authority.TENANT_ADMIN, + [ + {id: MenuId.home}, + {id: MenuId.alarms}, + {id: MenuId.dashboards}, + { + id: MenuId.entities, + pages: [ + {id: MenuId.devices}, + {id: MenuId.assets}, + {id: MenuId.entity_views} + ] + }, + { + id: MenuId.profiles, + pages: [ + {id: MenuId.device_profiles}, + {id: MenuId.asset_profiles} + ] + }, + {id: MenuId.customers}, + {id: MenuId.rule_chains}, + { + id: MenuId.edge_management, + pages: [ + {id: MenuId.edges}, + {id: MenuId.rulechain_templates} + ] + }, + { + id: MenuId.features, + pages: [ + {id: MenuId.otaUpdates}, + {id: MenuId.version_control} + ] + }, + { + id: MenuId.resources, + pages: [ + { + id: MenuId.widget_library, + pages: [ + {id: MenuId.widget_types}, + {id: MenuId.widgets_bundles} + ] + }, + {id: MenuId.images}, + {id: MenuId.scada_symbols}, + {id: MenuId.resources_library} + ] + }, + { + id: MenuId.notifications_center, + pages: [ + {id: MenuId.notification_inbox}, + {id: MenuId.notification_sent}, + {id: MenuId.notification_recipients}, + {id: MenuId.notification_templates}, + {id: MenuId.notification_rules} + ] + }, + {id: MenuId.api_usage}, + { + id: MenuId.settings, + pages: [ + {id: MenuId.home_settings}, + {id: MenuId.notification_settings}, + {id: MenuId.repository_settings}, + {id: MenuId.auto_commit_settings} + ] + }, + { + id: MenuId.security_settings, + pages: [ + {id: MenuId.audit_log} + ] + } + ] + ], + [ + Authority.CUSTOMER_USER, + [ + {id: MenuId.home}, + {id: MenuId.alarms}, + {id: MenuId.dashboards}, + { + id: MenuId.entities, + pages: [ + {id: MenuId.devices}, + {id: MenuId.assets}, + {id: MenuId.entity_views} + ] + }, + {id: MenuId.edges}, + { + id: MenuId.notifications_center, + pages: [ + {id: MenuId.notification_inbox} + ] + } + ] + ] +]); + +export const buildUserMenu = (authState: AuthState): Array => { + const references = defaultUserMenuMap.get(authState.authUser.authority); + return (references || []).map(ref => referenceToMenuSection(authState, ref)).filter(section => !!section); +}; + +const referenceToMenuSection = (authState: AuthState, reference: MenuReference): MenuSection | undefined => { + if (filterMenuReference(authState, reference)) { + const section = menuSectionMap.get(reference.id); + if (section) { + const result = deepClone(section); + if (reference.pages?.length) { + result.pages = reference.pages.map(page => + referenceToMenuSection(authState, page)).filter(page => !!page); + } + return result; + } else { + return undefined; + } + } else { + return undefined; + } +}; + +const filterMenuReference = (authState: AuthState, reference: MenuReference): boolean => { + const filter = menuFilters.get(reference.id); + if (filter) { + if (filter(authState)) { + if (reference.pages?.length) { + if (reference.pages.every(page => !filterMenuReference(authState, page))) { + return false; + } + } + return true; + } + return false; + } else { + return true; + } +}; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 309d82ebc0..c51be16c5c 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -19,8 +19,8 @@ import { select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { filter, map, take } from 'rxjs/operators'; -import { HomeSection, MenuSection } from '@core/services/menu.models'; -import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { buildUserMenu, HomeSection, MenuId, MenuSection } from '@core/services/menu.models'; +import { BehaviorSubject, ReplaySubject, Observable, Subject } from 'rxjs'; import { Authority } from '@shared/models/authority.enum'; import { AuthState } from '@core/auth/auth.models'; import { NavigationEnd, Router } from '@angular/router'; @@ -31,11 +31,14 @@ import { NavigationEnd, Router } from '@angular/router'; export class MenuService { currentMenuSections: Array; - menuSections$: Subject> = new BehaviorSubject>([]); + menuSections$: Subject> = new ReplaySubject>(); homeSections$: Subject> = new BehaviorSubject>([]); availableMenuLinks$ = this.menuSections$.pipe( map((items) => this.allMenuLinks(items)) ); + availableMenuSections$ = this.menuSections$.pipe( + map((items) => this.allMenuSections(items)) + ); constructor(private store: Store, private router: Router) { @@ -60,18 +63,16 @@ export class MenuService { let homeSections: Array; switch (authState.authUser.authority) { case Authority.SYS_ADMIN: - this.currentMenuSections = this.buildSysAdminMenu(); homeSections = this.buildSysAdminHome(); break; case Authority.TENANT_ADMIN: - this.currentMenuSections = this.buildTenantAdminMenu(authState); homeSections = this.buildTenantAdminHome(authState); break; case Authority.CUSTOMER_USER: - this.currentMenuSections = this.buildCustomerUserMenu(authState); homeSections = this.buildCustomerUserHome(authState); break; } + this.currentMenuSections = buildUserMenu(authState); this.updateOpenedMenuSections(); this.menuSections$.next(this.currentMenuSections); this.homeSections$.next(homeSections); @@ -89,214 +90,6 @@ export class MenuService { ); } - private buildSysAdminMenu(): Array { - const sections: Array = []; - sections.push( - { - id: 'home', - name: 'home.home', - type: 'link', - path: '/home', - icon: 'home' - }, - { - id: 'tenants', - name: 'tenant.tenants', - type: 'link', - path: '/tenants', - icon: 'supervisor_account' - }, - { - id: 'tenant_profiles', - name: 'tenant-profile.tenant-profiles', - type: 'link', - path: '/tenantProfiles', - icon: 'mdi:alpha-t-box' - }, - { - id: 'resources', - name: 'admin.resources', - type: 'toggle', - path: '/resources', - icon: 'folder', - pages: [ - { - id: 'widget_library', - name: 'widget.widget-library', - type: 'link', - path: '/resources/widgets-library', - icon: 'now_widgets', - pages: [ - { - id: 'widget_types', - name: 'widget.widgets', - type: 'link', - path: '/resources/widgets-library/widget-types', - icon: 'now_widgets' - }, - { - id: 'widgets_bundles', - name: 'widgets-bundle.widgets-bundles', - type: 'link', - path: '/resources/widgets-library/widgets-bundles', - icon: 'now_widgets' - } - ] - }, - { - id: 'images', - name: 'image.gallery', - type: 'link', - path: '/resources/images', - icon: 'filter' - }, - { - id: 'scada_symbols', - name: 'scada.symbols', - type: 'link', - path: '/resources/scada-symbols', - icon: 'view_in_ar' - }, - { - id: 'resources_library', - name: 'resource.resources-library', - type: 'link', - path: '/resources/resources-library', - icon: 'mdi:rhombus-split' - } - ] - }, - { - id: 'notifications_center', - name: 'notification.notification-center', - type: 'link', - path: '/notification', - icon: 'mdi:message-badge', - pages: [ - { - id: 'notification_inbox', - name: 'notification.inbox', - fullName: 'notification.notification-inbox', - type: 'link', - path: '/notification/inbox', - icon: 'inbox' - }, - { - id: 'notification_sent', - name: 'notification.sent', - fullName: 'notification.notification-sent', - type: 'link', - path: '/notification/sent', - icon: 'outbox' - }, - { - id: 'notification_recipients', - name: 'notification.recipients', - fullName: 'notification.notification-recipients', - type: 'link', - path: '/notification/recipients', - icon: 'contacts' - }, - { - id: 'notification_templates', - name: 'notification.templates', - fullName: 'notification.notification-templates', - type: 'link', - path: '/notification/templates', - icon: 'mdi:message-draw' - }, - { - id: 'notification_rules', - name: 'notification.rules', - fullName: 'notification.notification-rules', - type: 'link', - path: '/notification/rules', - icon: 'mdi:message-cog' - } - ] - }, - { - id: 'settings', - name: 'admin.settings', - type: 'link', - path: '/settings', - icon: 'settings', - pages: [ - { - id: 'general', - name: 'admin.general', - fullName: 'admin.general-settings', - type: 'link', - path: '/settings/general', - icon: 'settings_applications' - }, - { - id: 'mail_server', - name: 'admin.outgoing-mail', - type: 'link', - path: '/settings/outgoing-mail', - icon: 'mail' - }, - { - id: 'notification_settings', - name: 'admin.notifications', - fullName: 'admin.notifications-settings', - type: 'link', - path: '/settings/notifications', - icon: 'mdi:message-badge' - }, - { - id: 'queues', - name: 'admin.queues', - type: 'link', - path: '/settings/queues', - icon: 'swap_calls' - }, - { - id: 'mobile_app_settings', - name: 'admin.mobile-app.mobile-app', - fullName: 'admin.mobile-app.mobile-app', - type: 'link', - path: '/settings/mobile-app', - icon: 'smartphone' - } - ] - }, - { - id: 'security_settings', - name: 'security.security', - type: 'toggle', - path: '/security-settings', - icon: 'security', - pages: [ - { - id: 'security_settings_general', - name: 'admin.general', - fullName: 'security.general-settings', - type: 'link', - path: '/security-settings/general', - icon: 'settings_applications' - }, - { - id: '2fa', - name: 'admin.2fa.2fa', - type: 'link', - path: '/security-settings/2fa', - icon: 'mdi:two-factor-authentication' - }, - { - id: 'oauth2', - name: 'admin.oauth2.oauth2', - type: 'link', - path: '/security-settings/oauth2', - icon: 'mdi:shield-account' - } - ] - } - ); - return sections; - } - private buildSysAdminHome(): Array { const homeSections: Array = []; homeSections.push( @@ -374,321 +167,6 @@ export class MenuService { return homeSections; } - private buildTenantAdminMenu(authState: AuthState): Array { - const sections: Array = []; - sections.push( - { - id: 'home', - name: 'home.home', - type: 'link', - path: '/home', - icon: 'home' - }, - { - id: 'alarms', - name: 'alarm.alarms', - type: 'link', - path: '/alarms', - icon: 'mdi:alert-outline' - }, - { - id: 'dashboards', - name: 'dashboard.dashboards', - type: 'link', - path: '/dashboards', - icon: 'dashboards' - }, - { - id: 'entities', - name: 'entity.entities', - type: 'toggle', - path: '/entities', - icon: 'category', - pages: [ - { - id: 'devices', - name: 'device.devices', - type: 'link', - path: '/entities/devices', - icon: 'devices_other' - }, - { - id: 'assets', - name: 'asset.assets', - type: 'link', - path: '/entities/assets', - icon: 'domain' - }, - { - id: 'entity_views', - name: 'entity-view.entity-views', - type: 'link', - path: '/entities/entityViews', - icon: 'view_quilt' - } - ] - }, - { - id: 'profiles', - name: 'profiles.profiles', - type: 'toggle', - path: '/profiles', - icon: 'badge', - pages: [ - { - id: 'device_profiles', - name: 'device-profile.device-profiles', - type: 'link', - path: '/profiles/deviceProfiles', - icon: 'mdi:alpha-d-box' - }, - { - id: 'asset_profiles', - name: 'asset-profile.asset-profiles', - type: 'link', - path: '/profiles/assetProfiles', - icon: 'mdi:alpha-a-box' - } - ] - }, - { - id: 'customers', - name: 'customer.customers', - type: 'link', - path: '/customers', - icon: 'supervisor_account' - }, - { - id: 'rule_chains', - name: 'rulechain.rulechains', - type: 'link', - path: '/ruleChains', - icon: 'settings_ethernet' - } - ); - if (authState.edgesSupportEnabled) { - sections.push( - { - id: 'edge_management', - name: 'edge.management', - type: 'toggle', - path: '/edgeManagement', - icon: 'settings_input_antenna', - pages: [ - { - id: 'edges', - name: 'edge.instances', - fullName: 'edge.edge-instances', - type: 'link', - path: '/edgeManagement/instances', - icon: 'router' - }, - { - id: 'rulechain_templates', - name: 'edge.rulechain-templates', - fullName: 'edge.edge-rulechain-templates', - type: 'link', - path: '/edgeManagement/ruleChains', - icon: 'settings_ethernet' - } - ] - } - ); - } - sections.push( - { - id: 'features', - name: 'feature.advanced-features', - type: 'toggle', - path: '/features', - icon: 'construction', - pages: [ - { - id: 'otaUpdates', - name: 'ota-update.ota-updates', - type: 'link', - path: '/features/otaUpdates', - icon: 'memory' - }, - { - id: 'version_control', - name: 'version-control.version-control', - type: 'link', - path: '/features/vc', - icon: 'history' - } - ] - }, - { - id: 'resources', - name: 'admin.resources', - type: 'toggle', - path: '/resources', - icon: 'folder', - pages: [ - { - id: 'widget_library', - name: 'widget.widget-library', - type: 'link', - path: '/resources/widgets-library', - icon: 'now_widgets', - pages: [ - { - id: 'widget_types', - name: 'widget.widgets', - type: 'link', - path: '/resources/widgets-library/widget-types', - icon: 'now_widgets' - }, - { - id: 'widgets_bundles', - name: 'widgets-bundle.widgets-bundles', - type: 'link', - path: '/resources/widgets-library/widgets-bundles', - icon: 'now_widgets' - } - ] - }, - { - id: 'images', - name: 'image.gallery', - type: 'link', - path: '/resources/images', - icon: 'filter' - }, - { - id: 'scada_symbols', - name: 'scada.symbols', - type: 'link', - path: '/resources/scada-symbols', - icon: 'view_in_ar' - }, - { - id: 'resources_library', - name: 'resource.resources-library', - type: 'link', - path: '/resources/resources-library', - icon: 'mdi:rhombus-split' - } - ] - }, - { - id: 'notifications_center', - name: 'notification.notification-center', - type: 'link', - path: '/notification', - icon: 'mdi:message-badge', - pages: [ - { - id: 'notification_inbox', - name: 'notification.inbox', - fullName: 'notification.notification-inbox', - type: 'link', - path: '/notification/inbox', - icon: 'inbox' - }, - { - id: 'notification_sent', - name: 'notification.sent', - fullName: 'notification.notification-sent', - type: 'link', - path: '/notification/sent', - icon: 'outbox' - }, - { - id: 'notification_recipients', - name: 'notification.recipients', - fullName: 'notification.notification-recipients', - type: 'link', - path: '/notification/recipients', - icon: 'contacts' - }, - { - id: 'notification_templates', - name: 'notification.templates', - fullName: 'notification.notification-templates', - type: 'link', - path: '/notification/templates', - icon: 'mdi:message-draw' - }, - { - id: 'notification_rules', - name: 'notification.rules', - fullName: 'notification.notification-rules', - type: 'link', - path: '/notification/rules', - icon: 'mdi:message-cog' - } - ] - }, - { - id: 'api_usage', - name: 'api-usage.api-usage', - type: 'link', - path: '/usage', - icon: 'insert_chart' - }, - { - id: 'settings', - name: 'admin.settings', - type: 'link', - path: '/settings', - icon: 'settings', - pages: [ - { - id: 'home_settings', - name: 'admin.home', - fullName: 'admin.home-settings', - type: 'link', - path: '/settings/home', - icon: 'settings_applications' - }, - { - id: 'notification_settings', - name: 'admin.notifications', - fullName: 'admin.notifications-settings', - type: 'link', - path: '/settings/notifications', - icon: 'mdi:message-badge' - }, - { - id: 'repository_settings', - name: 'admin.repository', - fullName: 'admin.repository-settings', - type: 'link', - path: '/settings/repository', - icon: 'manage_history' - }, - { - id: 'auto_commit_settings', - name: 'admin.auto-commit', - fullName: 'admin.auto-commit-settings', - type: 'link', - path: '/settings/auto-commit', - icon: 'settings_backup_restore' - } - ] - }, - { - id: 'security_settings', - name: 'security.security', - type: 'toggle', - path: '/security-settings', - icon: 'security', - pages: [ - { - id: 'audit_log', - name: 'audit-log.audit-logs', - type: 'link', - path: '/security-settings/auditLogs', - icon: 'track_changes' - } - ] - } - ); - return sections; - } - private buildTenantAdminHome(authState: AuthState): Array { const homeSections: Array = []; homeSections.push( @@ -847,95 +325,6 @@ export class MenuService { return homeSections; } - private buildCustomerUserMenu(authState: AuthState): Array { - const sections: Array = []; - sections.push( - { - id: 'home', - name: 'home.home', - type: 'link', - path: '/home', - icon: 'home' - }, - { - id: 'alarms', - name: 'alarm.alarms', - type: 'link', - path: '/alarms', - icon: 'mdi:alert-outline' - }, - { - id: 'dashboards', - name: 'dashboard.dashboards', - type: 'link', - path: '/dashboards', - icon: 'dashboards' - }, - { - id: 'entities', - name: 'entity.entities', - type: 'toggle', - path: '/entities', - icon: 'category', - pages: [ - { - id: 'devices', - name: 'device.devices', - type: 'link', - path: '/entities/devices', - icon: 'devices_other' - }, - { - id: 'assets', - name: 'asset.assets', - type: 'link', - path: '/entities/assets', - icon: 'domain' - }, - { - id: 'entity_views', - name: 'entity-view.entity-views', - type: 'link', - path: '/entities/entityViews', - icon: 'view_quilt' - } - ] - } - ); - if (authState.edgesSupportEnabled) { - sections.push( - { - id: 'edges', - name: 'edge.edge-instances', - fullName: 'edge.edge-instances', - type: 'link', - path: '/edgeManagement/instances', - icon: 'router' - } - ); - } - sections.push( - { - id: 'notifications_center', - name: 'notification.notification-center', - type: 'link', - path: '/notification', - icon: 'mdi:message-badge', - pages: [ - { - id: 'notification_inbox', - name: 'notification.inbox', - fullName: 'notification.notification-inbox', - type: 'link', - path: '/notification/inbox', - icon: 'inbox' - } - ] - } - ); - return sections; - } - private buildCustomerUserHome(authState: AuthState): Array { const homeSections: Array = []; homeSections.push( @@ -1012,6 +401,17 @@ export class MenuService { return result; } + private allMenuSections(sections: Array): Array { + const result: Array = []; + for (const section of sections) { + result.push(section); + if (section.pages && section.pages.length) { + result.push(...this.allMenuSections(section.pages)); + } + } + return result; + } + public menuSections(): Observable> { return this.menuSections$; } @@ -1024,7 +424,11 @@ export class MenuService { return this.availableMenuLinks$; } - public menuLinkById(id: string): Observable { + public availableMenuSections(): Observable> { + return this.availableMenuSections$; + } + + public menuLinkById(id: MenuId | string): Observable { return this.availableMenuLinks$.pipe( map((links) => links.find(link => link.id === id)) ); diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 1c4d6c204d..a01826bd08 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -45,6 +45,7 @@ import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/m import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; import { ImageService } from '@core/http/image.service'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { MenuId } from '@core/services/menu.models'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -79,8 +80,7 @@ const routes: Routes = [ data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], breadcrumb: { - label: 'admin.resources', - icon: 'folder' + menuId: MenuId.resources } }, children: [ @@ -97,8 +97,7 @@ const routes: Routes = [ path: 'images', data: { breadcrumb: { - label: 'image.gallery', - icon: 'filter' + menuId: MenuId.images } }, children: [ @@ -117,8 +116,7 @@ const routes: Routes = [ path: 'scada-symbols', data: { breadcrumb: { - label: 'scada.symbols', - icon: 'view_in_ar' + menuId: MenuId.scada_symbols } }, children: [ @@ -153,8 +151,7 @@ const routes: Routes = [ path: 'resources-library', data: { breadcrumb: { - label: 'resource.resources-library', - icon: 'mdi:rhombus-split' + menuId: MenuId.resources_library } }, children: [ @@ -196,8 +193,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], showMainLoadingBar: false, breadcrumb: { - label: 'admin.settings', - icon: 'settings' + menuId: MenuId.settings } }, children: [ @@ -220,8 +216,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.general-settings', breadcrumb: { - label: 'admin.general', - icon: 'settings_applications' + menuId: MenuId.general } } }, @@ -233,8 +228,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.outgoing-mail-settings', breadcrumb: { - label: 'admin.outgoing-mail', - icon: 'mail' + menuId: MenuId.mail_server } } }, @@ -246,8 +240,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], title: 'admin.notifications-settings', breadcrumb: { - label: 'admin.notifications', - icon: 'mdi:message-badge' + menuId: MenuId.notification_settings } } }, @@ -255,8 +248,7 @@ const routes: Routes = [ path: 'queues', data: { breadcrumb: { - label: 'admin.queues', - icon: 'swap_calls' + menuId: MenuId.queues } }, children: [ @@ -297,8 +289,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN], title: 'admin.home-settings', breadcrumb: { - label: 'admin.home', - icon: 'settings_applications' + menuId: MenuId.home_settings } } }, @@ -310,8 +301,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN], title: 'admin.repository-settings', breadcrumb: { - label: 'admin.repository', - icon: 'manage_history' + menuId: MenuId.repository_settings } } }, @@ -323,8 +313,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN], title: 'admin.auto-commit-settings', breadcrumb: { - label: 'admin.auto-commit', - icon: 'settings_backup_restore' + menuId: MenuId.auto_commit_settings } } }, @@ -336,8 +325,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.mobile-app.mobile-app', breadcrumb: { - label: 'admin.mobile-app.mobile-app', - icon: 'smartphone' + menuId: MenuId.mobile_app_settings } } }, @@ -373,8 +361,7 @@ const routes: Routes = [ data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], breadcrumb: { - label: 'security.security', - icon: 'security' + menuId: MenuId.security_settings } }, children: [ @@ -397,8 +384,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.general', breadcrumb: { - label: 'admin.general', - icon: 'settings_applications' + menuId: MenuId.security_settings_general } } }, @@ -410,8 +396,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.2fa.2fa', breadcrumb: { - label: 'admin.2fa.2fa', - icon: 'mdi:two-factor-authentication' + menuId: MenuId.two_fa } } }, @@ -423,8 +408,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.oauth2.oauth2', breadcrumb: { - label: 'admin.oauth2.oauth2', - icon: 'mdi:shield-account' + menuId: MenuId.oauth2 } }, resolve: { diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts index 0f971a879d..3ada68643f 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { OAuth2Service } from '@core/http/oauth2.service'; import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; import { AlarmsMode } from '@shared/models/alarm.models'; +import { MenuId } from '@core/services/menu.models'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -41,8 +42,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], title: 'alarm.alarms', breadcrumb: { - label: 'alarm.alarms', - icon: 'mdi:alert-outline' + menuId: MenuId.alarms }, isPage: true, alarmsMode: AlarmsMode.ALL diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts index bc582da666..f76ceb86bc 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts @@ -21,6 +21,7 @@ import { ApiUsageComponent } from '@home/pages/api-usage/api-usage.component'; import { Dashboard } from '@shared/models/dashboard.models'; import { ResourcesService } from '@core/services/resources.service'; import { Observable } from 'rxjs'; +import { MenuId } from '@core/services/menu.models'; const apiUsageDashboardJson = '/assets/dashboard/api_usage.json'; @@ -38,8 +39,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN], title: 'api-usage.api-usage', breadcrumb: { - label: 'api-usage.api-usage', - icon: 'insert_chart' + menuId: MenuId.api_usage } }, resolve: { diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts index 6f7316b1ae..67229feace 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts @@ -24,14 +24,14 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { AssetProfilesTableConfigResolver } from './asset-profiles-table-config.resolver'; +import { MenuId } from '@core/services/menu.models'; export const assetProfilesRoutes: Routes = [ { path: 'assetProfiles', data: { breadcrumb: { - label: 'asset-profile.asset-profiles', - icon: 'mdi:alpha-a-box' + menuId: MenuId.asset_profiles } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts index 6ddc5735ab..05dec3b260 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts @@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { MenuId } from '@core/services/menu.models'; export const assetRoutes: Routes = [ { path: 'assets', data: { breadcrumb: { - label: 'asset.assets', - icon: 'domain' + menuId: MenuId.assets } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts index 768400a21e..5afeb2c694 100644 --- a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts @@ -18,6 +18,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; +import { MenuId } from '@core/services/menu.models'; export const auditLogsRoutes: Routes = [ { @@ -27,8 +28,7 @@ export const auditLogsRoutes: Routes = [ auth: [Authority.TENANT_ADMIN], title: 'audit-log.audit-logs', breadcrumb: { - label: 'audit-log.audit-logs', - icon: 'track_changes' + menuId: MenuId.audit_log }, isPage: true } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts index 7090ce7fc8..1339d55b40 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -31,14 +31,14 @@ import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.re import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { path: 'customers', data: { breadcrumb: { - label: 'customer.customers', - icon: 'supervisor_account' + menuId: MenuId.customers } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts index 223ce60825..28fc9d4e12 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts @@ -33,6 +33,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { MenuId } from '@core/services/menu.models'; @Injectable() export class DashboardResolver implements Resolve { @@ -66,8 +67,7 @@ const routes: Routes = [ path: 'dashboards', data: { breadcrumb: { - label: 'dashboard.dashboards', - icon: 'dashboard' + menuId: MenuId.dashboards } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts index 9626d8a616..66b7961914 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts @@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { MenuId } from '@core/services/menu.models'; export const deviceProfilesRoutes: Routes = [ { path: 'deviceProfiles', data: { breadcrumb: { - label: 'device-profile.device-profiles', - icon: 'mdi:alpha-d-box' + menuId: MenuId.device_profiles } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts index 5fa15dd375..01b4370e55 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts @@ -26,14 +26,14 @@ import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { assetRoutes } from '@home/pages/asset/asset-routing.module'; import { entityViewRoutes } from '@home/pages/entity-view/entity-view-routing.module'; +import { MenuId } from '@core/services/menu.models'; export const deviceRoutes: Routes = [ { path: 'devices', data: { breadcrumb: { - label: 'device.devices', - icon: 'devices_other' + menuId: MenuId.devices } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts index 484bb5336b..1302bfa140 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts @@ -41,14 +41,14 @@ import { } from '@home/pages/rulechain/rulechain-routing.module'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { path: 'edgeManagement', data: { breadcrumb: { - label: 'edge.management', - icon: 'settings_input_antenna' + menuId: MenuId.edge_management } }, children: [ @@ -64,8 +64,7 @@ const routes: Routes = [ path: 'instances', data: { breadcrumb: { - label: 'edge.instances', - icon: 'router' + menuId: MenuId.edges } }, children: [ @@ -307,8 +306,7 @@ const routes: Routes = [ path: 'ruleChains', data: { breadcrumb: { - label: 'edge.rulechain-templates', - icon: 'settings_ethernet' + menuId: MenuId.rulechain_templates } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts index d08f48bf7b..4666b7234f 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts @@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { MenuId } from '@core/services/menu.models'; export const entityViewRoutes: Routes = [ { path: 'entityViews', data: { breadcrumb: { - label: 'entity-view.entity-views', - icon: 'view_quilt' + menuId: MenuId.entity_views } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts b/ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts index 7be7e896e8..0fc21f90f7 100644 --- a/ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts @@ -19,6 +19,7 @@ import { Authority } from '@shared/models/authority.enum'; import { NgModule } from '@angular/core'; import { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module'; import { vcRoutes } from '@home/pages/vc/vc-routing.module'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { @@ -26,8 +27,7 @@ const routes: Routes = [ data: { auth: [Authority.TENANT_ADMIN], breadcrumb: { - label: 'feature.advanced-features', - icon: 'construction' + menuId: MenuId.features } }, children: [ 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 303a1bce4e..153fba8c93 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 @@ -34,6 +34,7 @@ import { import { EntityKeyType } from '@shared/models/query/query.models'; import { ResourcesService } from '@core/services/resources.service'; import { isDefinedAndNotNull } from '@core/utils'; +import { MenuId } from '@core/services/menu.models'; const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json'; const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json'; @@ -125,8 +126,7 @@ const routes: Routes = [ auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], title: 'home.home', breadcrumb: { - label: 'home.home', - icon: 'home' + menuId: MenuId.home } }, resolve: { diff --git a/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts b/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts index 41f49ee708..b1bf35bcc6 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts @@ -25,6 +25,7 @@ import { RecipientTableConfigResolver } from '@home/pages/notification/recipient import { TemplateTableConfigResolver } from '@home/pages/notification/template/template-table-config.resolver'; import { RuleTableConfigResolver } from '@home/pages/notification/rule/rule-table-config.resolver'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { @@ -33,8 +34,7 @@ const routes: Routes = [ data: { auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], breadcrumb: { - label: 'notification.notification-center', - icon: 'mdi:message-badge' + menuId: MenuId.notifications_center }, routerTabsHeaderComponent: SendNotificationButtonComponent }, @@ -54,8 +54,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], title: 'notification.inbox', breadcrumb: { - label: 'notification.inbox', - icon: 'inbox' + menuId: MenuId.notification_inbox } }, resolve: { @@ -69,8 +68,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'notification.sent', breadcrumb: { - label: 'notification.sent', - icon: 'outbox' + menuId: MenuId.notification_sent } }, resolve: { @@ -84,8 +82,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'notification.templates', breadcrumb: { - label: 'notification.templates', - icon: 'mdi:message-draw' + menuId: MenuId.notification_templates } }, resolve: { @@ -99,8 +96,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'notification.recipients', breadcrumb: { - label: 'notification.recipients', - icon: 'contacts' + menuId: MenuId.notification_recipients }, }, resolve: { @@ -114,8 +110,7 @@ const routes: Routes = [ auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'notification.rules', breadcrumb: { - label: 'notification.rules', - icon: 'mdi:message-cog' + menuId: MenuId.notification_rules } }, resolve: { diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts index 78cb83bd9f..6904b72045 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts @@ -23,14 +23,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { MenuId } from '@core/services/menu.models'; export const otaUpdatesRoutes: Routes = [ { path: 'otaUpdates', data: { breadcrumb: { - label: 'ota-update.ota-updates', - icon: 'memory' + menuId: MenuId.otaUpdates } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts index aa0ffdee0e..d64463064a 100644 --- a/ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts @@ -19,6 +19,7 @@ import { Authority } from '@shared/models/authority.enum'; import { NgModule } from '@angular/core'; import { deviceProfilesRoutes } from '@home/pages/device-profile/device-profile-routing.module'; import { assetProfilesRoutes } from '@home/pages/asset-profile/asset-profile-routing.module'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { @@ -26,8 +27,7 @@ const routes: Routes = [ data: { auth: [Authority.TENANT_ADMIN], breadcrumb: { - label: 'profiles.profiles', - icon: 'badge' + menuId: MenuId.profiles } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts index 47fa4970c6..c341ae6848 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts @@ -39,7 +39,7 @@ import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainPageComponent } from '@home/pages/rulechain/rulechain-page.component'; import { RuleNodeComponentDescriptor } from '@shared/models/rule-node.models'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; -import { ItemBufferService } from '@core/public-api'; +import { ItemBufferService, MenuId } from '@core/public-api'; import { MODULES_MAP } from '@shared/public-api'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -127,8 +127,7 @@ const routes: Routes = [ path: 'ruleChains', data: { breadcrumb: { - label: 'rulechain.rulechains', - icon: 'settings_ethernet' + menuId: MenuId.rule_chains } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts index c414238733..a9fb7c961c 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts @@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { path: 'tenantProfiles', data: { breadcrumb: { - label: 'tenant-profile.tenant-profiles', - icon: 'mdi:alpha-t-box' + menuId: MenuId.tenant_profiles } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts index b8ea897b66..141a799dce 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts @@ -25,14 +25,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { MenuId } from '@core/services/menu.models'; const routes: Routes = [ { path: 'tenants', data: { breadcrumb: { - label: 'tenant.tenants', - icon: 'supervisor_account' + menuId: MenuId.tenants } }, children: [ diff --git a/ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts b/ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts index 2708a42c11..1d4112021a 100644 --- a/ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts @@ -19,6 +19,7 @@ import { RouterModule, Routes } from '@angular/router'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { Authority } from '@shared/models/authority.enum'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; +import { MenuId } from '@core/services/menu.models'; export const vcRoutes: Routes = [ { @@ -29,8 +30,7 @@ export const vcRoutes: Routes = [ auth: [Authority.TENANT_ADMIN], title: 'version-control.version-control', breadcrumb: { - label: 'version-control.version-control', - icon: 'history' + menuId: MenuId.version_control } } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index c808bae163..046737ac10 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -33,6 +33,7 @@ import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types- import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { MenuId } from '@core/services/menu.models'; export interface WidgetEditorData { widgetTypeDetails: WidgetTypeDetails; @@ -108,8 +109,7 @@ const widgetTypesRoutes: Routes = [ path: 'widget-types', data: { breadcrumb: { - label: 'widget.widgets', - icon: 'now_widgets' + menuId: MenuId.widget_types } }, children: [ @@ -157,8 +157,7 @@ const widgetsBundlesRoutes: Routes = [ path: 'widgets-bundles', data: { breadcrumb: { - label: 'widgets-bundle.widgets-bundles', - icon: 'now_widgets' + menuId: MenuId.widgets_bundles } }, children: [ @@ -235,8 +234,7 @@ export const widgetsLibraryRoutes: Routes = [ data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], breadcrumb: { - label: 'widget.widget-library', - icon: 'now_widgets' + menuId: MenuId.widget_library } }, children: [ diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index 6045f38ff2..506555d9e8 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -18,7 +18,7 @@

{{ breadcrumb.ignoreTranslate - ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label)) + ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate)) : (breadcrumb.label | translate) }}

@@ -41,6 +41,6 @@ {{ breadcrumb.icon }} {{ breadcrumb.ignoreTranslate - ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label)) + ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate)) : (breadcrumb.label | translate) }} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index b5255bb0d1..718a1b8900 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -14,16 +14,18 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, Subject, Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { BehaviorSubject, mergeMap, Subject, Subscription } from 'rxjs'; import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; -import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { guid } from '@core/utils'; import { BroadcastService } from '@core/services/broadcast.service'; import { ActiveComponentService } from '@core/services/active-component.service'; import { UtilsService } from '@core/services/utils.service'; +import { MenuSection } from '@core/services/menu.models'; +import { MenuService } from '@core/services/menu.service'; @Component({ selector: 'tb-breadcrumb', @@ -44,7 +46,9 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { this.activeComponentValue = activeComponent; if (this.activeComponentValue && this.activeComponentValue.updateBreadcrumbs) { this.updateBreadcrumbsSubscription = this.activeComponentValue.updateBreadcrumbs.subscribe(() => { - this.breadcrumbs$.next(this.buildBreadCrumbs(this.activatedRoute.snapshot)); + this.menuService.availableMenuSections().pipe(first()).subscribe((sections) => { + this.breadcrumbs$.next(this.buildBreadCrumbs(this.activatedRoute.snapshot, sections)); + }); }); } } @@ -54,7 +58,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { routerEventsSubscription = this.router.events.pipe( filter((event) => event instanceof NavigationEnd ), distinctUntilChanged(), - map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) ) + mergeMap(() => this.menuService.availableMenuSections().pipe(first())), + map( (sections) => this.buildBreadCrumbs(this.activatedRoute.snapshot, sections) ) ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) ); activeComponentSubscription = this.activeComponentService.onActiveComponentChanged().subscribe(comp => this.setActiveComponent(comp)); @@ -69,6 +74,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { private activeComponentService: ActiveComponentService, private cd: ChangeDetectorRef, private translate: TranslateService, + private menuService: MenuService, public utils: UtilsService) { } @@ -96,7 +102,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { return child; } - buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array = [], + buildBreadCrumbs(route: ActivatedRouteSnapshot, availableMenuSections: MenuSection[], + breadcrumbs: Array = [], lastChild?: ActivatedRouteSnapshot): Array { if (!lastChild) { lastChild = this.lastChild(route); @@ -105,18 +112,19 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { if (route.routeConfig && route.routeConfig.data) { const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; if (breadcrumbConfig && !breadcrumbConfig.skip) { - let label; - let labelFunction; - let ignoreTranslate; + let labelFunction: () => string; + let ignoreTranslate: boolean; + const section: MenuSection = breadcrumbConfig.menuId ? + availableMenuSections.find(menu => menu.id === breadcrumbConfig.menuId) : null; + const label = section?.name || breadcrumbConfig.label || 'home.home'; if (breadcrumbConfig.labelFunction) { labelFunction = () => this.activeComponentValue ? - breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data) : breadcrumbConfig.label; + breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data) : label; ignoreTranslate = true; } else { - label = breadcrumbConfig.label || 'home.home'; ignoreTranslate = false; } - const icon = breadcrumbConfig.icon || 'home'; + const icon = section?.icon || breadcrumbConfig.icon || 'home'; const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ]; const breadcrumb = { id: guid(), @@ -131,12 +139,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { } } if (route.firstChild) { - return this.buildBreadCrumbs(route.firstChild, newBreadcrumbs, lastChild); + return this.buildBreadCrumbs(route.firstChild, availableMenuSections, newBreadcrumbs, lastChild); } return newBreadcrumbs; } - trackByBreadcrumbs(index: number, breadcrumb: BreadCrumb){ + trackByBreadcrumbs(_index: number, breadcrumb: BreadCrumb){ return breadcrumb.id; } } diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts index 175a0b374d..5adf8b3aa2 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -17,6 +17,7 @@ import { ActivatedRouteSnapshot, Params } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { MenuId } from '@core/services/menu.models'; export interface BreadCrumb extends HasUUID{ label: string; @@ -31,7 +32,8 @@ export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, transla export interface BreadCrumbConfig { labelFunction: BreadCrumbLabelFunction; - label: string; - icon: string; + menuId?: MenuId; + label?: string; + icon?: string; skip: boolean; } From 9f7d6737b36b3f2367a9644e9c65fcd4e5f71fa4 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 19 Aug 2024 15:20:47 +0300 Subject: [PATCH 233/331] added @Length validations according to database restriction --- .../server/controller/MobileAppControllerTest.java | 2 +- .../server/common/data/domain/Domain.java | 2 ++ .../server/common/data/mobile/MobileApp.java | 3 ++- .../server/common/data/oauth2/OAuth2Client.java | 14 +++++++++++++- .../dao/sql/oauth2/OAuth2ClientRepository.java | 4 ++-- dao/src/main/resources/sql/schema-entities.sql | 2 +- .../server/dao/service/DomainServiceTest.java | 4 ++-- .../server/dao/service/MobileAppServiceTest.java | 2 +- 8 files changed, 24 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java index 23b1f47c91..d19178242a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java @@ -92,7 +92,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { mobileApp.setAppSecret("short"); doPost("/api/mobileApp", mobileApp) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("appSecret must be at least 16 characters"))); + .andExpect(statusReason(containsString("appSecret must be at least 16 and max 2048 characters"))); } @Test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java b/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java index e2414f11d8..9efbad8a25 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; @EqualsAndHashCode(callSuper = true) @Data @@ -35,6 +36,7 @@ public class Domain extends BaseData implements HasTenantId, HasName { private TenantId tenantId; @Schema(description = "Domain name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "name") private String name; @Schema(description = "Whether OAuth2 settings are enabled or not") private boolean oauth2Enabled; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java index 3dad679532..ad207e5635 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java @@ -38,10 +38,11 @@ public class MobileApp extends BaseData implements HasTenantId, Has private TenantId tenantId; @Schema(description = "Application package name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "pkgName") private String pkgName; @Schema(description = "Application secret. The length must be at least 16 characters", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty - @Length(min = 16, message = "must be at least 16 characters") + @Length(fieldName = "appSecret", min = 16, max = 2048, message = "must be at least 16 and max 2048 characters") private String appSecret; @Schema(description = "Whether OAuth2 settings are enabled or not") private boolean oauth2Enabled; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java index f9b93b00e1..0a9924785b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java @@ -42,42 +42,54 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp private TenantId tenantId; @Schema(description = "Oauth2 client title") @NotBlank - @Length(max = 100, message = "cannot be longer than 100 chars") + @Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars") private String title; @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull private OAuth2MapperConfig mapperConfig; @Schema(description = "OAuth2 client ID. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "clientId") private String clientId; @Schema(description = "OAuth2 client secret. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "clientSecret", max = 2048) private String clientSecret; @Schema(description = "Authorization URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "authorizationUri") private String authorizationUri; @Schema(description = "Access token URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "accessTokenUri") private String accessTokenUri; @Schema(description = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty + @Length(fieldName = "scope") private List scope; @Schema(description = "User info URI of the OAuth2 provider") + @Length(fieldName = "userInfoUri") private String userInfoUri; @Schema(description = "Name of the username attribute in OAuth2 provider response. Cannot be empty") @NotBlank + @Length(fieldName = "userNameAttributeName") private String userNameAttributeName; @Schema(description = "JSON Web Key URI of the OAuth2 provider") + @Length(fieldName = "jwkSetUri") private String jwkSetUri; @Schema(description = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "clientAuthenticationMethod") private String clientAuthenticationMethod; @Schema(description = "OAuth2 provider label. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @Length(fieldName = "loginButtonLabel") private String loginButtonLabel; @Schema(description = "Log in button icon for OAuth2 provider") + @Length(fieldName = "loginButtonIcon") private String loginButtonIcon; @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") + @Length(fieldName = "platforms") private List platforms; @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", requiredMode = Schema.RequiredMode.REQUIRED) private JsonNode additionalInfo; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java index 2d50761945..d431cf2db5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java @@ -57,13 +57,13 @@ public interface OAuth2ClientRepository extends JpaRepository findByDomainId(UUID domainId); + List findByDomainId(@Param("domainId") UUID domainId); @Query("SELECT c " + "FROM OAuth2ClientEntity c " + "LEFT JOIN MobileAppOauth2ClientEntity mc on mc.oauth2ClientId = c.id " + "WHERE mc.mobileAppId = :mobileAppId ") - List findByMobileAppId(UUID mobileAppId); + List findByMobileAppId(@Param("mobileAppId") UUID mobileAppId); @Query("SELECT m.appSecret " + "FROM MobileAppEntity m " + diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9731b8126c..7ce086f3e5 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -586,7 +586,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client ( id uuid NOT NULL CONSTRAINT oauth2_client_pkey PRIMARY KEY, created_time bigint NOT NULL, tenant_id uuid NOT NULL, - title varchar(100) NOT NULL, + title varchar(100) NOT NULL, additional_info varchar, client_id varchar(255), client_secret varchar(2048), diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java index 844adec27b..d5d033722f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java @@ -99,7 +99,7 @@ public class DomainServiceTest extends AbstractServiceTest { } @Test - public void tesGetDomainInfo() { + public void testGetDomainInfo() { OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client"); OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); PageData infos = oAuth2ClientService.findOAuth2ClientInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10)); @@ -115,7 +115,7 @@ public class DomainServiceTest extends AbstractServiceTest { //find clients by domain name List oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByDomainName(savedDomain.getName()); - assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getName(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString()))); + assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getLoginButtonLabel(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString()))); } private Domain constructDomain(TenantId tenantId, String domainName, boolean oauth2Enabled, boolean propagateToEdge) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java index 77788c98b8..91bb72a92d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java @@ -101,7 +101,7 @@ public class MobileAppServiceTest extends AbstractServiceTest { //find clients by MobileApp name List oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(savedMobileApp.getName(), null); - assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getName(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString()))); + assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getLoginButtonLabel(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString()))); } private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) { From bc296d698e5a4c7a43556a704487a5b24f260679 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 19 Aug 2024 15:25:04 +0300 Subject: [PATCH 234/331] UI: Fixed adaptive manage layout dialog --- .../layout/manage-dashboard-layouts-dialog.component.html | 2 +- .../layout/manage-dashboard-layouts-dialog.component.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html index 5df01bccef..961f6b4846 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html @@ -25,7 +25,7 @@ close -
+
dashboard.layout
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss index 8c192bcca5..d18a48fbaf 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss @@ -34,7 +34,7 @@ $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text); } .tb-layout-preview { - width: 120%; + width: 100%; background-color: rgba(mat.get-color-from-palette($tb-primary, 50), 0.6); padding: 35px; From 49da787fc409b7de088599489cae29294343c851 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 19 Aug 2024 16:59:12 +0300 Subject: [PATCH 235/331] UI: Change entity version conflict error handling --- .../entity-conflict.interceptor.ts | 2 +- .../dashboard-page.component.ts | 29 +++++++++++++++---- .../entity/entity-details-panel.component.ts | 10 +++++-- .../widget/widget-component.service.ts | 2 +- .../pages/widget/widget-editor.component.ts | 13 +++++---- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts b/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts index e34aca7007..e4db59ec66 100644 --- a/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts @@ -75,7 +75,7 @@ export class EntityConflictInterceptor implements HttpInterceptor { return next.handle(this.updateRequestVersion(request)); } (request.params as HttpParams & { interceptorConfig: InterceptorConfig }).interceptorConfig.ignoreErrors = true; - return throwError(() => new Error(error.error.message)); + return throwError(() => error); } return of(null); }) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index f7efca5d40..109d5f014b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -86,7 +86,7 @@ import { Authority } from '@shared/models/authority.enum'; import { DialogService } from '@core/services/dialog.service'; import { EntityService } from '@core/http/entity.service'; import { AliasController } from '@core/api/alias-controller'; -import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, of, Subject, Subscription, throwError } from 'rxjs'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardService } from '@core/http/dashboard.service'; import { @@ -156,6 +156,7 @@ import { MoveWidgetsDialogComponent, MoveWidgetsDialogResult } from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; +import { HttpStatusCode } from '@angular/common/http'; // @dynamic @Component({ @@ -1203,15 +1204,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC data: widget }; this.window.parent.postMessage(JSON.stringify(message), '*'); + this.setEditMode(false, false); } else { + let reInitDashboard = false; this.dashboardService.saveDashboard(this.dashboard).pipe( - catchError(() => { - this.setEditMode(false, true); - return of(null); + catchError((err) => { + if (err.status === HttpStatusCode.Conflict) { + reInitDashboard = true; + return this.dashboardService.getDashboard(this.dashboard.id.id).pipe( + map(dashboard => this.dashboardUtils.validateAndUpdateDashboard(dashboard)) + ); + } + return throwError(() => err); }) - ).subscribe(() => { - this.dashboard.version = this.dashboard.version + 1; + ).subscribe((dashboard) => { this.setEditMode(false, false); + this.dashboard = dashboard; + if (reInitDashboard) { + const dashboardPageInitData: DashboardPageInitData = { + dashboard, + currentDashboardId: dashboard.id ? dashboard.id.id : null, + widgetEditMode: false, + singlePageMode: false + }; + this.init(dashboardPageInitData); + } }); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index c283a57c4f..1ec56028af 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -40,11 +40,12 @@ import { UntypedFormGroup } from '@angular/forms'; import { EntityComponent } from './entity.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; -import { Observable, of, ReplaySubject, Subscription } from 'rxjs'; +import { Observable, ReplaySubject, Subscription, throwError } from 'rxjs'; import { MatTab, MatTabGroup } from '@angular/material/tabs'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; import { deepClone, mergeDeep } from '@core/utils'; import { catchError } from 'rxjs/operators'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ selector: 'tb-entity-details-panel', @@ -290,7 +291,12 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV } this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity) .pipe( - catchError(() => of(this.entity)) + catchError((err) => { + if (err.status === HttpStatusCode.Conflict) { + return this.entitiesTableConfig.loadEntity(this.currentEntityId); + } + return throwError(() => err); + }) ) .subscribe( (entity) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index aa7f62e33b..508d07ea0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -113,7 +113,7 @@ export class WidgetComponentService { hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, defaultConfig: this.utils.editWidgetInfo.defaultConfig - }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined, null + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined, undefined ); } const initSubject = new ReplaySubject(); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 614787233d..3ca2036369 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -59,12 +59,13 @@ import { SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult } from '@home/pages/widget/save-widget-type-as-dialog.component'; -import { forkJoin, mergeMap, of, Subscription } from 'rxjs'; +import { forkJoin, mergeMap, of, Subscription, throwError } from 'rxjs'; import { ResizeObserver } from '@juggle/resize-observer'; import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; import { Observable } from 'rxjs/internal/Observable'; import { catchError, map, tap } from 'rxjs/operators'; import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; +import { HttpStatusCode } from '@angular/common/http'; import Timeout = NodeJS.Timeout; // @dynamic @@ -583,9 +584,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe } return of(widgetTypeDetails); }), - catchError(() => { - this.undoWidget(); - return of(null); + catchError((err) => { + if (id && err.status === HttpStatusCode.Conflict) { + return this.widgetService.getWidgetTypeById(id.id); + } + return throwError(() => err); }), ).subscribe({ next: (widgetTypeDetails) => { @@ -619,7 +622,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe config.title = this.widget.widgetName; this.widget.defaultConfig = JSON.stringify(config); this.isDirty = false; - this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined, null).pipe( + this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined, undefined).pipe( mergeMap((widget) => { if (saveWidgetAsData.widgetBundleId) { return this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, widget.fqn).pipe( From d7ba724f6a14cf340dae8819cb3574e6a9afe987 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 19 Aug 2024 17:20:53 +0300 Subject: [PATCH 236/331] UI: Fixed adaptive manage layout dialog --- .../layout/add-new-breakpoint-dialog.component.html | 2 +- .../layout/manage-dashboard-layouts-dialog.component.html | 2 +- .../layout/manage-dashboard-layouts-dialog.component.scss | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html index edb87a6f79..8baab0f295 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html @@ -24,7 +24,7 @@ close -
+
layout.breakpoint diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html index 961f6b4846..71adbc91c8 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html @@ -25,7 +25,7 @@ close -
+
dashboard.layout
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss index d18a48fbaf..0933cafaff 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss @@ -185,6 +185,14 @@ $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text); } } +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + width: 550px; + max-width: 100%; + padding: 16px; + } +} + ::ng-deep { /* Alarm tooltip with side-to-side movement */ .tb-layout-error-tooltip-right { From 16b8fa23823071400f50c807da2dc41ad9ce0268 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 19 Aug 2024 17:27:03 +0300 Subject: [PATCH 237/331] UI: Fixed created default layout --- .../layout/manage-dashboard-layouts-dialog.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts index 77786e1cae..b91673ab17 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts @@ -293,8 +293,10 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent Date: Mon, 19 Aug 2024 17:46:56 +0300 Subject: [PATCH 238/331] fixed service tests --- .../server/dao/mobile/MobileAppServiceImpl.java | 14 +++++--------- .../dao/sql/oauth2/OAuth2ClientRepository.java | 2 +- .../server/dao/service/DomainServiceTest.java | 5 +++-- .../server/dao/service/MobileAppServiceTest.java | 5 +++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index 3089cc9ef4..f5b558cdcc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.mobile; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,11 +34,11 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; import org.thingsboard.server.dao.service.Validator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -62,13 +61,10 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil MobileApp savedMobileApp = mobileAppDao.save(tenantId, mobileApp); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); return savedMobileApp; - } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("mobile_app_unq_key")) { - throw new DataValidationException("Mobile app with such package already exists!"); - } else { - throw t; - } + } catch (Exception e) { + checkConstraintViolation(e, + Map.of("mobile_app_unq_key", "Mobile app with such package already exists!")); + throw e; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java index d431cf2db5..49b23ac7cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java @@ -40,7 +40,7 @@ public interface OAuth2ClientRepository extends JpaRepository findEnabledByDomainNameAndPlatformType(@Param("domainName") String domainName, @Param("platformFilter") String platformFilter); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java index d5d033722f..734e30137a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java @@ -49,6 +49,7 @@ public class DomainServiceTest extends AbstractServiceTest { @After public void after() { domainService.deleteByTenantId(TenantId.SYS_TENANT_ID); + oAuth2ClientService.deleteByTenantId(TenantId.SYS_TENANT_ID); } @Test @@ -102,7 +103,7 @@ public class DomainServiceTest extends AbstractServiceTest { public void testGetDomainInfo() { OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client"); OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); - PageData infos = oAuth2ClientService.findOAuth2ClientInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10)); + List oAuth2ClientInfosByIds = oAuth2ClientService.findOAuth2ClientInfosByIds(TenantId.SYS_TENANT_ID, List.of(savedOauth2Client.getId())); Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "test.domain.com", true, true); Domain savedDomain = domainService.saveDomain(SYSTEM_TENANT_ID, domain); @@ -111,7 +112,7 @@ public class DomainServiceTest extends AbstractServiceTest { // check domain info DomainInfo retrievedInfo = domainService.findDomainInfoById(SYSTEM_TENANT_ID, savedDomain.getId()); - assertThat(retrievedInfo).isEqualTo(new DomainInfo(savedDomain, infos.getData())); + assertThat(retrievedInfo).isEqualTo(new DomainInfo(savedDomain, oAuth2ClientInfosByIds)); //find clients by domain name List oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByDomainName(savedDomain.getName()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java index 91bb72a92d..ec093d0947 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java @@ -49,6 +49,7 @@ public class MobileAppServiceTest extends AbstractServiceTest { @After public void after() { mobileAppService.deleteByTenantId(TenantId.SYS_TENANT_ID); + oAuth2ClientService.deleteByTenantId(TenantId.SYS_TENANT_ID); } @Test @@ -88,7 +89,7 @@ public class MobileAppServiceTest extends AbstractServiceTest { public void tesGetMobileAppInfo() { OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client"); OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); - PageData infos = oAuth2ClientService.findOAuth2ClientInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10)); + List oAuth2ClientInfosByIds = oAuth2ClientService.findOAuth2ClientInfosByIds(TenantId.SYS_TENANT_ID, List.of(savedOauth2Client.getId())); MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.app", true); MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp); @@ -97,7 +98,7 @@ public class MobileAppServiceTest extends AbstractServiceTest { // check MobileApp info MobileAppInfo retrievedInfo = mobileAppService.findMobileAppInfoById(SYSTEM_TENANT_ID, savedMobileApp.getId()); - assertThat(retrievedInfo).isEqualTo(new MobileAppInfo(savedMobileApp, infos.getData())); + assertThat(retrievedInfo).isEqualTo(new MobileAppInfo(savedMobileApp, oAuth2ClientInfosByIds)); //find clients by MobileApp name List oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(savedMobileApp.getName(), null); From 7044be4d537e64058e23be6b303cff3919f65fd9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 19 Aug 2024 18:31:06 +0300 Subject: [PATCH 239/331] UI: Rename input property in entity-list component --- .../notification/sent/sent-notification-dialog.component.html | 2 +- .../src/app/shared/components/entity/entity-list.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 2bc622e982..e9e34c24bc 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -104,7 +104,7 @@ ; @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; @@ -194,7 +194,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV (entities) => { this.entities = entities; this.entityListFormGroup.get('entities').setValue(this.entities); - if (this.syncedIdListPropagator && this.modelValue.length !== entities.length) { + if (this.syncIdsWithDB && this.modelValue.length !== entities.length) { this.modelValue = entities.map(entity => entity.id.id); this.propagateChange(this.modelValue); } From dc336c37526704c89ad8a0e0c38a1cc77430fdf7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 19 Aug 2024 18:36:31 +0300 Subject: [PATCH 240/331] reload entity fix --- .../dashboard-page/dashboard-page.component.ts | 9 +++++---- .../home/components/entity/entities-table.component.ts | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 109d5f014b..5af1833794 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1218,16 +1218,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC return throwError(() => err); }) ).subscribe((dashboard) => { - this.setEditMode(false, false); - this.dashboard = dashboard; if (reInitDashboard) { const dashboardPageInitData: DashboardPageInitData = { dashboard, currentDashboardId: dashboard.id ? dashboard.id.id : null, - widgetEditMode: false, - singlePageMode: false + widgetEditMode: this.widgetEditMode, + singlePageMode: this.singlePageMode }; this.init(dashboardPageInitData); + } else { + this.dashboard = dashboard; + this.setEditMode(false, false); } }); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index dd3e846d92..99b6275dfe 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -402,7 +402,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.cd.detectChanges(); } - updateData(closeDetails: boolean = true) { + updateData(closeDetails: boolean = true, reloadEntity: boolean = true) { if (closeDetails) { this.isDetailsOpen = false; } @@ -427,7 +427,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa timePageLink.endTime = interval.endTime; } this.dataSource.loadEntities(this.pageLink); - if (this.isDetailsOpen && this.entityDetailsPanel) { + if (reloadEntity && this.isDetailsOpen && this.entityDetailsPanel) { this.entityDetailsPanel.reloadEntity(); } } @@ -511,7 +511,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } onEntityUpdated(entity: BaseData) { - this.updateData(false); + this.updateData(false, false); this.entitiesTableConfig.entityUpdated(entity); } From a9d211ffeedb219a0a9041c408c712b1300e9335 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 20 Aug 2024 09:53:37 +0300 Subject: [PATCH 241/331] UI: Update widget bundle for tank scada symbol --- .../scada_water_system_symbols.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json index 224938c019..1ec69037ae 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -47,6 +47,21 @@ "vertical_wheel_valve", "horizontal_ball_valve", "vertical_ball_valve", - "vertical_tank" + "vertical_tank", + "stand_vertical_tank", + "cylindrical_tank", + "stand_cylindrical_tank", + "vertical_short_tank", + "stand_vertical_short_tank", + "large_cylindrical_tank", + "large_stand_cylindrical_tank", + "large_vertical_tank", + "large_stand_vertical_tank", + "horizontal_tank", + "stand_horizontal_tank", + "spherical_tank", + "small_spherical_tank", + "elevated_tank", + "pool" ] } \ No newline at end of file From f58376e3123759d1e295782d7687ef54e9cc2249 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 20 Aug 2024 11:25:29 +0300 Subject: [PATCH 242/331] fixed tests --- .../server/actors/ActorSystemContext.java | 15 ++++++++++ .../actors/ruleChain/DefaultTbContext.java | 18 +++++++++++ .../rule/engine/api/TbContext.java | 9 ++++++ .../rule/engine/util/TenantIdLoader.java | 12 ++++++++ .../rule/engine/util/TenantIdLoaderTest.java | 30 +++++++++++++++++++ 5 files changed, 84 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b125daa591..5966716ed8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -68,17 +68,20 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -370,6 +373,18 @@ public class ActorSystemContext { @Getter private NotificationRuleService notificationRuleService; + @Autowired + @Getter + private OAuth2ClientService oAuth2ClientService; + + @Autowired + @Getter + private DomainService domainService; + + @Autowired + @Getter + private MobileAppService mobileAppService; + @Autowired @Getter private SlackService slackService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ca404fc78a..ec10402821 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -83,17 +83,20 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -825,6 +828,21 @@ class DefaultTbContext implements TbContext { return mainCtx.getNotificationRuleService(); } + @Override + public OAuth2ClientService getOAuth2ClientService() { + return mainCtx.getOAuth2ClientService(); + } + + @Override + public DomainService getDomainService() { + return mainCtx.getDomainService(); + } + + @Override + public MobileAppService getMobileAppService() { + return mainCtx.getMobileAppService(); + } + @Override public SlackService getSlackService() { return mainCtx.getSlackService(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index fad13373c0..7dd4505f29 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -55,17 +55,20 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -346,6 +349,12 @@ public interface TbContext { NotificationRuleService getNotificationRuleService(); + OAuth2ClientService getOAuth2ClientService(); + + DomainService getDomainService(); + + MobileAppService getMobileAppService(); + SlackService getSlackService(); boolean isExternalNodeForceAck(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 152e193ea4..1e1b031259 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -26,13 +26,16 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.QueueStatsId; @@ -145,6 +148,15 @@ public class TenantIdLoader { case QUEUE_STATS: tenantEntity = ctx.getQueueStatsService().findQueueStatsById(ctxTenantId, new QueueStatsId(id)); break; + case OAUTH2_CLIENT: + tenantEntity = ctx.getOAuth2ClientService().findOAuth2ClientById(ctxTenantId, new OAuth2ClientId(id)); + break; + case DOMAIN: + tenantEntity = ctx.getDomainService().findDomainById(ctxTenantId, new DomainId(id)); + break; + case MOBILE_APP: + tenantEntity = ctx.getMobileAppService().findMobileAppById(ctxTenantId, new MobileAppId(id)); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 47fa03e08e..3e0e479c80 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -51,10 +52,12 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.rpc.Rpc; @@ -66,12 +69,15 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -139,6 +145,12 @@ public class TenantIdLoaderTest { private NotificationRuleService notificationRuleService; @Mock private QueueStatsService queueStatsService; + @Mock + private OAuth2ClientService oAuth2ClientService; + @Mock + private DomainService domainService; + @Mock + private MobileAppService mobileAppService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -362,6 +374,24 @@ public class TenantIdLoaderTest { when(ctx.getQueueStatsService()).thenReturn(queueStatsService); doReturn(queueStats).when(queueStatsService).findQueueStatsById(eq(tenantId), any()); break; + case OAUTH2_CLIENT: + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTenantId(tenantId); + when(ctx.getOAuth2ClientService()).thenReturn(oAuth2ClientService); + doReturn(oAuth2Client).when(oAuth2ClientService).findOAuth2ClientById(eq(tenantId), any()); + break; + case DOMAIN: + Domain domain = new Domain(); + domain.setTenantId(tenantId); + when(ctx.getDomainService()).thenReturn(domainService); + doReturn(domain).when(domainService).findDomainById(eq(tenantId), any()); + break; + case MOBILE_APP: + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + when(ctx.getMobileAppService()).thenReturn(mobileAppService); + doReturn(mobileApp).when(mobileAppService).findMobileAppById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } From 8febc1ed42951c5b57eb88df7b3442d46c06a98c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 12:42:44 +0300 Subject: [PATCH 243/331] UI: Fix menuSections subject. --- ui-ngx/src/app/core/services/menu.service.ts | 2 +- ui-ngx/src/app/shared/components/breadcrumb.component.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index c51be16c5c..80111ebf41 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -31,7 +31,7 @@ import { NavigationEnd, Router } from '@angular/router'; export class MenuService { currentMenuSections: Array; - menuSections$: Subject> = new ReplaySubject>(); + menuSections$: Subject> = new ReplaySubject>(1); homeSections$: Subject> = new BehaviorSubject>([]); availableMenuLinks$ = this.menuSections$.pipe( map((items) => this.allMenuLinks(items)) diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 718a1b8900..cc4ea56bc4 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -15,10 +15,10 @@ /// import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, mergeMap, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; -import { distinctUntilChanged, filter, first, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, switchMap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { guid } from '@core/utils'; import { BroadcastService } from '@core/services/broadcast.service'; @@ -58,7 +58,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { routerEventsSubscription = this.router.events.pipe( filter((event) => event instanceof NavigationEnd ), distinctUntilChanged(), - mergeMap(() => this.menuService.availableMenuSections().pipe(first())), + switchMap(() => this.menuService.availableMenuSections().pipe(first())), map( (sections) => this.buildBreadCrumbs(this.activatedRoute.snapshot, sections) ) ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) ); From eca50b018fa55816a35edc8591aa4991a5e6c97c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 20 Aug 2024 12:51:30 +0300 Subject: [PATCH 244/331] rid from 'registration' --- .../auth/oauth2/BasicOAuth2ClientMapper.java | 6 +-- .../auth/oauth2/CustomOAuth2ClientMapper.java | 6 +-- .../auth/oauth2/GithubOAuth2ClientMapper.java | 6 +-- .../auth/oauth2/OAuth2ClientMapper.java | 2 +- .../Oauth2AuthenticationSuccessHandler.java | 8 ++-- .../server/common/data/domain/DomainInfo.java | 2 +- .../server/dao/model/sql/DomainEntity.java | 5 +-- .../server/dao/model/sql/MobileAppEntity.java | 5 +-- .../dao/model/sql/OAuth2ClientEntity.java | 41 +++++++++---------- .../HybridClientRegistrationRepository.java | 24 +++++------ 10 files changed, 48 insertions(+), 57 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 72380fabcc..cad0eecfcd 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -33,12 +33,12 @@ import java.util.Map; public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, oAuth2Client); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index abbd57e562..e9c2b9583d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -41,10 +41,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client auth2Client) { + OAuth2MapperConfig config = auth2Client.getMapperConfig(); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, auth2Client); } private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java index f7af72ce69..889b7a828d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -49,13 +49,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private OAuth2Configuration oAuth2Configuration; @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); Map attributes = token.getPrincipal().getAttributes(); OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oAuth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, oAuth2Client); } private synchronized String getEmail(String emailUrl, String oauth2Token) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index 35bf09fa46..d65f3f66ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -21,5 +21,5 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.service.security.model.SecurityUser; public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client registration); + SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 102709b717..b85e618a6e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -97,19 +97,19 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS try { OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; - OAuth2Client registration = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(token.getAuthorizedClientRegistrationId()))); + OAuth2Client oauth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(token.getAuthorizedClientRegistrationId()))); OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient( token.getAuthorizedClientRegistrationId(), token.getPrincipal().getName()); - OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType()); + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType()); SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), - registration); + oauth2Client); clearAuthenticationAttributes(request, response); JwtPair tokenPair = tokenFactory.createTokenPair(securityUser); getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(baseUrl, tokenPair)); - systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null); + systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, oauth2Client.getName(), null); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java index 335d5b1604..55871e5d6b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java @@ -28,7 +28,7 @@ import java.util.List; @Schema public class DomainInfo extends Domain { - @Schema(description = "List of available oauth2 client registration") + @Schema(description = "List of available oauth2 clients") private List oauth2ClientInfos; public DomainInfo(Domain domain, List oauth2ClientInfos) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java index 069481a205..1b0c7a4183 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java @@ -49,13 +49,10 @@ public class DomainEntity extends BaseSqlEntity { private Boolean propagateToEdge; public DomainEntity(Domain domain) { - if (domain.getId() != null) { - this.setUuid(domain.getId().getId()); - } + super(domain); if (domain.getTenantId() != null) { this.tenantId = domain.getTenantId().getId(); } - this.setCreatedTime(domain.getCreatedTime()); this.name = domain.getName(); this.oauth2Enabled = domain.isOauth2Enabled(); this.propagateToEdge = domain.isPropagateToEdge(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java index 52bba805fe..0313864d09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java @@ -53,13 +53,10 @@ public class MobileAppEntity extends BaseSqlEntity { } public MobileAppEntity(MobileApp mobile) { - if (mobile.getId() != null) { - this.setUuid(mobile.getId().getId()); - } + super(mobile); if (mobile.getTenantId() != null) { this.tenantId = mobile.getTenantId().getId(); } - this.setCreatedTime(mobile.getCreatedTime()); this.pkgName = mobile.getPkgName(); this.appSecret = mobile.getAppSecret(); this.oauth2Enabled = mobile.isOauth2Enabled(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java index b9792de53d..8428d346ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java @@ -118,29 +118,26 @@ public class OAuth2ClientEntity extends BaseSqlEntity { super(); } - public OAuth2ClientEntity(OAuth2Client registration) { - if (registration.getId() != null) { - this.setUuid(registration.getId().getId()); + public OAuth2ClientEntity(OAuth2Client oAuth2Client) { + super(oAuth2Client); + if (oAuth2Client.getTenantId() != null) { + this.tenantId = oAuth2Client.getTenantId().getId(); } - this.setCreatedTime(registration.getCreatedTime()); - if (registration.getTenantId() != null) { - this.tenantId = registration.getTenantId().getId(); - } - this.title = registration.getTitle(); - this.clientId = registration.getClientId(); - this.clientSecret = registration.getClientSecret(); - this.authorizationUri = registration.getAuthorizationUri(); - this.tokenUri = registration.getAccessTokenUri(); - this.scope = registration.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); - this.platforms = registration.getPlatforms() != null ? registration.getPlatforms().stream().map(Enum::name).reduce((result, element) -> result + "," + element).orElse("") : ""; - this.userInfoUri = registration.getUserInfoUri(); - this.userNameAttributeName = registration.getUserNameAttributeName(); - this.jwkSetUri = registration.getJwkSetUri(); - this.clientAuthenticationMethod = registration.getClientAuthenticationMethod(); - this.loginButtonLabel = registration.getLoginButtonLabel(); - this.loginButtonIcon = registration.getLoginButtonIcon(); - this.additionalInfo = registration.getAdditionalInfo(); - OAuth2MapperConfig mapperConfig = registration.getMapperConfig(); + this.title = oAuth2Client.getTitle(); + this.clientId = oAuth2Client.getClientId(); + this.clientSecret = oAuth2Client.getClientSecret(); + this.authorizationUri = oAuth2Client.getAuthorizationUri(); + this.tokenUri = oAuth2Client.getAccessTokenUri(); + this.scope = oAuth2Client.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.platforms = oAuth2Client.getPlatforms() != null ? oAuth2Client.getPlatforms().stream().map(Enum::name).reduce((result, element) -> result + "," + element).orElse("") : ""; + this.userInfoUri = oAuth2Client.getUserInfoUri(); + this.userNameAttributeName = oAuth2Client.getUserNameAttributeName(); + this.jwkSetUri = oAuth2Client.getJwkSetUri(); + this.clientAuthenticationMethod = oAuth2Client.getClientAuthenticationMethod(); + this.loginButtonLabel = oAuth2Client.getLoginButtonLabel(); + this.loginButtonIcon = oAuth2Client.getLoginButtonIcon(); + this.additionalInfo = oAuth2Client.getAdditionalInfo(); + OAuth2MapperConfig mapperConfig = oAuth2Client.getMapperConfig(); if (mapperConfig != null) { this.allowUserCreation = mapperConfig.isAllowUserCreation(); this.activateUser = mapperConfig.isActivateUser(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 0315f93628..8009cf98f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -41,20 +41,20 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep null : toSpringClientRegistration(registration); } - private ClientRegistration toSpringClientRegistration(OAuth2Client registration){ - String registrationId = registration.getUuidId().toString(); + private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ + String registrationId = oAuth2Client.getUuidId().toString(); return ClientRegistration.withRegistrationId(registrationId) - .clientName(registration.getName()) - .clientId(registration.getClientId()) - .authorizationUri(registration.getAuthorizationUri()) - .clientSecret(registration.getClientSecret()) - .tokenUri(registration.getAccessTokenUri()) - .scope(registration.getScope()) + .clientName(oAuth2Client.getName()) + .clientId(oAuth2Client.getClientId()) + .authorizationUri(oAuth2Client.getAuthorizationUri()) + .clientSecret(oAuth2Client.getClientSecret()) + .tokenUri(oAuth2Client.getAccessTokenUri()) + .scope(oAuth2Client.getScope()) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .userInfoUri(registration.getUserInfoUri()) - .userNameAttributeName(registration.getUserNameAttributeName()) - .jwkSetUri(registration.getJwkSetUri()) - .clientAuthenticationMethod(registration.getClientAuthenticationMethod().equals("POST") ? + .userInfoUri(oAuth2Client.getUserInfoUri()) + .userNameAttributeName(oAuth2Client.getUserNameAttributeName()) + .jwkSetUri(oAuth2Client.getJwkSetUri()) + .clientAuthenticationMethod(oAuth2Client.getClientAuthenticationMethod().equals("POST") ? ClientAuthenticationMethod.CLIENT_SECRET_POST : ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .redirectUri(defaultRedirectUriTemplate) .build(); From bc844d3b7fba4866f359ced454b5774200d404ac Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 13:11:57 +0300 Subject: [PATCH 245/331] UI: Remove redudant disabled field from menu section --- ui-ngx/src/app/core/services/menu.models.ts | 1 - ui-ngx/src/app/modules/home/components/router-tabs.component.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 703397fee1..e775dd75bd 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -29,7 +29,6 @@ export interface MenuSection { icon: string; pages?: Array; opened?: boolean; - disabled?: boolean; rootOnly?: boolean; } diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.ts b/ui-ngx/src/app/modules/home/components/router-tabs.component.ts index 306e1b5af9..681558deb0 100644 --- a/ui-ngx/src/app/modules/home/components/router-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.ts @@ -107,7 +107,7 @@ export class RouterTabsComponent extends PageComponent implements OnInit { if (found) { const rootPath = sectionPath.substring(0, sectionPath.length - found.path.length); const isRoot = rootPath === ''; - const tabs: Array = found ? found.pages.filter(page => !page.disabled && (!page.rootOnly || isRoot)) : []; + const tabs: Array = found ? found.pages.filter(page => !page.rootOnly || isRoot) : []; return tabs.map((tab) => ({...tab, path: rootPath + tab.path})); } return []; From e97e57a38fb700d07a901ac5a2070168e6cf8a62 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 13:25:57 +0300 Subject: [PATCH 246/331] UI: Menu optimization --- ui-ngx/src/app/core/services/menu.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 80111ebf41..cfa555d627 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -30,13 +30,13 @@ import { NavigationEnd, Router } from '@angular/router'; }) export class MenuService { - currentMenuSections: Array; - menuSections$: Subject> = new ReplaySubject>(1); - homeSections$: Subject> = new BehaviorSubject>([]); - availableMenuLinks$ = this.menuSections$.pipe( + private currentMenuSections: Array; + private menuSections$: Subject> = new ReplaySubject>(1); + private homeSections$: Subject> = new BehaviorSubject>([]); + private availableMenuLinks$ = this.menuSections$.pipe( map((items) => this.allMenuLinks(items)) ); - availableMenuSections$ = this.menuSections$.pipe( + private availableMenuSections$ = this.menuSections$.pipe( map((items) => this.allMenuSections(items)) ); From c4a129a0bf15d9cea331eb817ecdd59c9edad6a3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 13:39:16 +0300 Subject: [PATCH 247/331] UI: Menu improvements. --- .../widget/lib/home-page/quick-link.component.ts | 8 +++++--- ui-ngx/src/app/modules/home/menu/menu-link.component.html | 2 +- .../src/app/modules/home/menu/menu-toggle.component.html | 2 +- .../src/app/shared/components/breadcrumb.component.html | 8 ++------ ui-ngx/src/app/shared/components/breadcrumb.component.ts | 5 ----- ui-ngx/src/app/shared/components/breadcrumb.ts | 1 - 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts index 7a2e7704f2..efc624f90d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts @@ -49,6 +49,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { deepClone } from '@core/utils'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; @Component({ selector: 'tb-quick-link', @@ -114,6 +115,7 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control private fb: UntypedFormBuilder, private menuService: MenuService, public translate: TranslateService, + private customTranslate: CustomTranslatePipe, @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { super(store); } @@ -135,7 +137,7 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control this.updateView(modelValue); }), map(value => value ? (typeof value === 'string' ? value : - ((value as any).translated ? value.name : this.translate.instant(value.name))) : ''), + ((value as any).translated ? value.name : this.customTranslate.transform(value.name))) : ''), distinctUntilChanged(), switchMap(name => this.fetchLinks(name) ), share() @@ -202,7 +204,7 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control } displayLinkFn = (link?: MenuSection): string | undefined => - link ? ((link as any).translated ? link.name : this.translate.instant(link.fullName || link.name)) : undefined; + link ? ((link as any).translated ? link.name : this.customTranslate.transform(link.fullName || link.name)) : undefined; fetchLinks(searchText?: string): Observable> { this.searchText = searchText; @@ -228,7 +230,7 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control map((links) => { const result = deepClone(links); for (const link of result) { - link.name = this.translate.instant(link.fullName || link.name); + link.name = this.customTranslate.transform(link.fullName || link.name); (link as any).translated = true; } return result; diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.html b/ui-ngx/src/app/modules/home/menu/menu-link.component.html index b1d7d00bcd..c0c098d5b6 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -17,5 +17,5 @@ --> {{section.icon}} - {{section.name | translate}} + {{section.name | customTranslate}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index 9375b04500..13eb0e2095 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -17,7 +17,7 @@ --> {{section.icon}} - {{section.name | translate}} + {{section.name | customTranslate}} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index 506555d9e8..3397d74e0d 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -17,9 +17,7 @@ -->

- {{ breadcrumb.ignoreTranslate - ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate)) - : (breadcrumb.label | translate) }} + {{ breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate) }}

@@ -40,7 +38,5 @@ {{ breadcrumb.icon }} - {{ breadcrumb.ignoreTranslate - ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate)) - : (breadcrumb.label | translate) }} + {{ breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate) }} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index cc4ea56bc4..9965560b55 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -113,16 +113,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; if (breadcrumbConfig && !breadcrumbConfig.skip) { let labelFunction: () => string; - let ignoreTranslate: boolean; const section: MenuSection = breadcrumbConfig.menuId ? availableMenuSections.find(menu => menu.id === breadcrumbConfig.menuId) : null; const label = section?.name || breadcrumbConfig.label || 'home.home'; if (breadcrumbConfig.labelFunction) { labelFunction = () => this.activeComponentValue ? breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data) : label; - ignoreTranslate = true; - } else { - ignoreTranslate = false; } const icon = section?.icon || breadcrumbConfig.icon || 'home'; const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ]; @@ -130,7 +126,6 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { id: guid(), label, labelFunction, - ignoreTranslate, icon, link, queryParams: null diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts index 5adf8b3aa2..9c85cc5a19 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -22,7 +22,6 @@ import { MenuId } from '@core/services/menu.models'; export interface BreadCrumb extends HasUUID{ label: string; labelFunction?: () => string; - ignoreTranslate: boolean; icon: string; link: any[]; queryParams: Params; From 4a5dabdad8913c845bd8ce616ae52a61965b390e Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 14:00:15 +0300 Subject: [PATCH 248/331] UI: Improve menu translation --- ui-ngx/src/app/core/services/menu.models.ts | 1 + .../widget/lib/home-page/quick-link.component.ts | 9 ++++++--- .../src/app/modules/home/menu/menu-link.component.html | 2 +- .../src/app/modules/home/menu/menu-toggle.component.html | 2 +- .../src/app/shared/components/breadcrumb.component.html | 4 ++-- ui-ngx/src/app/shared/components/breadcrumb.component.ts | 2 ++ ui-ngx/src/app/shared/components/breadcrumb.ts | 1 + 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index e775dd75bd..7afe08f929 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -30,6 +30,7 @@ export interface MenuSection { pages?: Array; opened?: boolean; rootOnly?: boolean; + customTranslate?: boolean; } export interface MenuReference { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts index efc624f90d..2662605e2a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts @@ -137,7 +137,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control this.updateView(modelValue); }), map(value => value ? (typeof value === 'string' ? value : - ((value as any).translated ? value.name : this.customTranslate.transform(value.name))) : ''), + ((value as any).translated ? value.name + : value.customTranslate ? this.customTranslate.transform(value.name) : this.translate.instant(value.name))) : ''), distinctUntilChanged(), switchMap(name => this.fetchLinks(name) ), share() @@ -204,7 +205,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control } displayLinkFn = (link?: MenuSection): string | undefined => - link ? ((link as any).translated ? link.name : this.customTranslate.transform(link.fullName || link.name)) : undefined; + link ? ((link as any).translated ? link.name : link.customTranslate ? this.customTranslate.transform(link.fullName || link.name) + : this.translate.instant(link.fullName || link.name)) : undefined; fetchLinks(searchText?: string): Observable> { this.searchText = searchText; @@ -230,7 +232,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control map((links) => { const result = deepClone(links); for (const link of result) { - link.name = this.customTranslate.transform(link.fullName || link.name); + link.name = link.customTranslate ? this.customTranslate.transform(link.fullName || link.name) + : this.translate.instant(link.fullName || link.name); (link as any).translated = true; } return result; diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.html b/ui-ngx/src/app/modules/home/menu/menu-link.component.html index c0c098d5b6..fea7165214 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -17,5 +17,5 @@ --> {{section.icon}} - {{section.name | customTranslate}} + {{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index 13eb0e2095..3d488e9229 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -17,7 +17,7 @@ --> {{section.icon}} - {{section.name | customTranslate}} + {{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index 3397d74e0d..a80d3b375d 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -17,7 +17,7 @@ -->

- {{ breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate) }} + {{ breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.customTranslate ? (breadcrumb.label | customTranslate) : (breadcrumb.label | translate)) }}

@@ -38,5 +38,5 @@ {{ breadcrumb.icon }} - {{ breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.label | customTranslate) }} + {{ breadcrumb.labelFunction ? breadcrumb.labelFunction() : (breadcrumb.customTranslate ? (breadcrumb.label | customTranslate) : (breadcrumb.label | translate)) }} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 9965560b55..23c9abe337 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -116,6 +116,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { const section: MenuSection = breadcrumbConfig.menuId ? availableMenuSections.find(menu => menu.id === breadcrumbConfig.menuId) : null; const label = section?.name || breadcrumbConfig.label || 'home.home'; + const customTranslate = section?.customTranslate || false; if (breadcrumbConfig.labelFunction) { labelFunction = () => this.activeComponentValue ? breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data) : label; @@ -125,6 +126,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { const breadcrumb = { id: guid(), label, + customTranslate, labelFunction, icon, link, diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts index 9c85cc5a19..1f5ad82820 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -21,6 +21,7 @@ import { MenuId } from '@core/services/menu.models'; export interface BreadCrumb extends HasUUID{ label: string; + customTranslate: boolean; labelFunction?: () => string; icon: string; link: any[]; From 4f9c01040b4ddda21b50d80ce386c9e5979bcf0c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 20 Aug 2024 14:21:07 +0300 Subject: [PATCH 249/331] fixed tests --- .../server/controller/HomePageApiTest.java | 12 +++++++++++- .../org/thingsboard/server/edge/OAuth2EdgeTest.java | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index 69f3c2a7db..88aa903c76 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -50,6 +50,8 @@ import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.TbApiUsageStateClient; +import org.thingsboard.server.dao.domain.DomainService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -77,6 +79,12 @@ public class HomePageApiTest extends AbstractControllerTest { @Autowired private AdminSettingsService adminSettingsService; + @Autowired + private DomainService domainService; + + @Autowired + private OAuth2ClientService oAuth2ClientService; + @MockBean private MailService mailService; @@ -363,7 +371,7 @@ public class HomePageApiTest extends AbstractControllerTest { OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); - Domain domain = createDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); + Domain domain = createDomain(TenantId.SYS_TENANT_ID, "my.home.domain", true, true); doPost("/api/domain?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), domain, Domain.class); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); @@ -377,6 +385,8 @@ public class HomePageApiTest extends AbstractControllerTest { adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "notifications"); adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "twoFaSettings"); adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms"); + oAuth2ClientService.deleteOauth2ClientsByTenantId(TenantId.SYS_TENANT_ID); + domainService.deleteDomainsByTenantId(TenantId.SYS_TENANT_ID); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java index e84cd41543..bb9073298d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.oauth2.MapperType; @@ -31,6 +32,7 @@ import java.util.Collections; import java.util.UUID; @DaoSqlTest +@Ignore("Ignored till fixed") public class OAuth2EdgeTest extends AbstractEdgeTest { @Test @@ -41,7 +43,7 @@ public class OAuth2EdgeTest extends AbstractEdgeTest { edgeImitator.allowIgnoredTypes(); edgeImitator.expectMessageAmount(1); OAuth2Client oAuth2Client = createDefaultOAuth2Info(); - oAuth2Client = doPost("/api/oauth2/config", oAuth2Client, OAuth2Client.class); + oAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg); @@ -66,6 +68,7 @@ public class OAuth2EdgeTest extends AbstractEdgeTest { private OAuth2Client validRegistrationInfo() { OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTitle(UUID.randomUUID().toString()); oAuth2Client.setClientId(UUID.randomUUID().toString()); oAuth2Client.setClientSecret(UUID.randomUUID().toString()); oAuth2Client.setAuthorizationUri(UUID.randomUUID().toString()); From 74e9a60bbe3fa888e9e466b7258e22f9d72b632f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 20 Aug 2024 14:42:48 +0300 Subject: [PATCH 250/331] refactoring --- .../org/thingsboard/server/controller/OAuth2Controller.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 4d0dc61750..0eddf8332b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -138,10 +138,7 @@ public class OAuth2Controller extends BaseController { public List findTenantOAuth2ClientInfosByIds( @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { - List oAuth2ClientIds = new ArrayList<>(); - for (UUID oauth2ClientId : clientIds) { - oAuth2ClientIds.add(new OAuth2ClientId(oauth2ClientId)); - } + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); } From 72134abbf9a5bd03dd4efcd6e7c53b76f2fc0972 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 14:46:28 +0300 Subject: [PATCH 251/331] UI: Home links refactoring. --- ui-ngx/src/app/core/services/menu.models.ts | 118 ++++++- ui-ngx/src/app/core/services/menu.service.ts | 324 +----------------- .../navigation-cards-widget.component.html | 2 +- .../lib/navigation-cards-widget.component.ts | 10 +- .../home-links/home-links.component.html | 2 +- 5 files changed, 126 insertions(+), 330 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 7afe08f929..e27d015fbe 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -38,15 +38,14 @@ export interface MenuReference { pages?: Array; } -export interface HomeSection { +export interface HomeSectionReference { name: string; - places: Array; + places: Array; } -export interface HomeSectionPlace { +export interface HomeSection { name: string; - icon: string; - path: string; + places: Array; } export enum MenuId { @@ -768,11 +767,108 @@ const defaultUserMenuMap = new Map([ ] ]); +const defaultHomeSectionMap = new Map([ + [ + Authority.SYS_ADMIN, + [ + { + name: 'tenant.management', + places: [MenuId.tenants, MenuId.tenant_profiles] + }, + { + name: 'widget.management', + places: [MenuId.widget_library] + }, + { + name: 'admin.system-settings', + places: [MenuId.general, MenuId.mail_server, + MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.two_fa, MenuId.resources_library, MenuId.queues] + } + ] + ], + [ + Authority.TENANT_ADMIN, + [ + { + name: 'rulechain.management', + places: [MenuId.rule_chains] + }, + { + name: 'customer.management', + places: [MenuId.customers] + }, + { + name: 'asset.management', + places: [MenuId.assets, MenuId.asset_profiles] + }, + { + name: 'device.management', + places: [MenuId.devices, MenuId.device_profiles, MenuId.otaUpdates] + }, + { + name: 'entity-view.management', + places: [MenuId.entity_views] + }, + { + name: 'edge.management', + places: [MenuId.edges, MenuId.rulechain_templates] + }, + { + name: 'dashboard.management', + places: [MenuId.widget_library, MenuId.dashboards] + }, + { + name: 'version-control.management', + places: [MenuId.version_control] + }, + { + name: 'audit-log.audit', + places: [MenuId.audit_log, MenuId.api_usage] + }, + { + name: 'admin.system-settings', + places: [MenuId.home_settings, MenuId.resources_library, MenuId.repository_settings, MenuId.auto_commit_settings] + } + ] + ], + [ + Authority.CUSTOMER_USER, + [ + { + name: 'asset.view-assets', + places: [MenuId.assets] + }, + { + name: 'device.view-devices', + places: [MenuId.devices] + }, + { + name: 'entity-view.management', + places: [MenuId.entity_views] + }, + { + name: 'edge.management', + places: [MenuId.edges] + }, + { + name: 'dashboard.view-dashboards', + places: [MenuId.dashboards] + } + ] + ] +]); + export const buildUserMenu = (authState: AuthState): Array => { const references = defaultUserMenuMap.get(authState.authUser.authority); return (references || []).map(ref => referenceToMenuSection(authState, ref)).filter(section => !!section); }; +export const buildUserHome = (authState: AuthState, availableMenuSections: MenuSection[]): Array => { + const references = defaultHomeSectionMap.get(authState.authUser.authority); + return (references || []).map(ref => + homeReferenceToHomeSection(availableMenuSections, ref)).filter(section => !!section); +}; + const referenceToMenuSection = (authState: AuthState, reference: MenuReference): MenuSection | undefined => { if (filterMenuReference(authState, reference)) { const section = menuSectionMap.get(reference.id); @@ -807,3 +903,15 @@ const filterMenuReference = (authState: AuthState, reference: MenuReference): bo return true; } }; + +const homeReferenceToHomeSection = (availableMenuSections: MenuSection[], reference: HomeSectionReference): HomeSection | undefined => { + const places = reference.places.map(id => availableMenuSections.find(m => m.id === id)).filter(p => !!p); + if (places.length) { + return { + name: reference.name, + places + }; + } else { + return undefined; + } +}; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index cfa555d627..90976e282c 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -19,9 +19,8 @@ import { select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { filter, map, take } from 'rxjs/operators'; -import { buildUserMenu, HomeSection, MenuId, MenuSection } from '@core/services/menu.models'; -import { BehaviorSubject, ReplaySubject, Observable, Subject } from 'rxjs'; -import { Authority } from '@shared/models/authority.enum'; +import { buildUserHome, buildUserMenu, HomeSection, MenuId, MenuSection } from '@core/services/menu.models'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; import { AuthState } from '@core/auth/auth.models'; import { NavigationEnd, Router } from '@angular/router'; @@ -32,13 +31,11 @@ export class MenuService { private currentMenuSections: Array; private menuSections$: Subject> = new ReplaySubject>(1); - private homeSections$: Subject> = new BehaviorSubject>([]); + private homeSections$: Subject> = new ReplaySubject>(1); + private availableMenuSections$: Subject> = new ReplaySubject>(1); private availableMenuLinks$ = this.menuSections$.pipe( map((items) => this.allMenuLinks(items)) ); - private availableMenuSections$ = this.menuSections$.pipe( - map((items) => this.allMenuSections(items)) - ); constructor(private store: Store, private router: Router) { @@ -60,21 +57,12 @@ export class MenuService { this.store.pipe(select(selectAuth), take(1)).subscribe( (authState: AuthState) => { if (authState.authUser) { - let homeSections: Array; - switch (authState.authUser.authority) { - case Authority.SYS_ADMIN: - homeSections = this.buildSysAdminHome(); - break; - case Authority.TENANT_ADMIN: - homeSections = this.buildTenantAdminHome(authState); - break; - case Authority.CUSTOMER_USER: - homeSections = this.buildCustomerUserHome(authState); - break; - } this.currentMenuSections = buildUserMenu(authState); this.updateOpenedMenuSections(); this.menuSections$.next(this.currentMenuSections); + const availableMenuSections = this.allMenuSections(this.currentMenuSections); + this.availableMenuSections$.next(availableMenuSections); + const homeSections = buildUserHome(authState, availableMenuSections); this.homeSections$.next(homeSections); } } @@ -90,304 +78,6 @@ export class MenuService { ); } - private buildSysAdminHome(): Array { - const homeSections: Array = []; - homeSections.push( - { - name: 'tenant.management', - places: [ - { - name: 'tenant.tenants', - icon: 'supervisor_account', - path: '/tenants' - }, - { - name: 'tenant-profile.tenant-profiles', - icon: 'mdi:alpha-t-box', - path: '/tenantProfiles' - }, - ] - }, - { - name: 'widget.management', - places: [ - { - name: 'widget.widget-library', - icon: 'now_widgets', - path: '/resources/widgets-library', - } - ] - }, - { - name: 'admin.system-settings', - places: [ - { - name: 'admin.general', - icon: 'settings_applications', - path: '/settings/general' - }, - { - name: 'admin.outgoing-mail', - icon: 'mail', - path: '/settings/outgoing-mail' - }, - { - name: 'admin.sms-provider', - icon: 'sms', - path: '/settings/sms-provider' - }, - { - name: 'admin.security-settings', - icon: 'security', - path: '/settings/security-settings' - }, - { - name: 'admin.oauth2.oauth2', - icon: 'security', - path: '/settings/oauth2' - }, - { - name: 'admin.2fa.2fa', - icon: 'mdi:two-factor-authentication', - path: '/settings/2fa' - }, - { - name: 'resource.resources-library', - icon: 'folder', - path: '/settings/resources-library' - }, - { - name: 'admin.queues', - icon: 'swap_calls', - path: '/settings/queues' - }, - ] - } - ); - return homeSections; - } - - private buildTenantAdminHome(authState: AuthState): Array { - const homeSections: Array = []; - homeSections.push( - { - name: 'rulechain.management', - places: [ - { - name: 'rulechain.rulechains', - icon: 'settings_ethernet', - path: '/ruleChains' - } - ] - }, - { - name: 'customer.management', - places: [ - { - name: 'customer.customers', - icon: 'supervisor_account', - path: '/customers' - } - ] - }, - { - name: 'asset.management', - places: [ - { - name: 'asset.assets', - icon: 'domain', - path: '/assets' - }, - { - name: 'asset-profile.asset-profiles', - icon: 'mdi:alpha-a-box', - path: '/profiles/assetProfiles' - } - ] - }, - { - name: 'device.management', - places: [ - { - name: 'device.devices', - icon: 'devices_other', - path: '/devices' - }, - { - name: 'device-profile.device-profiles', - icon: 'mdi:alpha-d-box', - path: '/profiles/deviceProfiles' - }, - { - name: 'ota-update.ota-updates', - icon: 'memory', - path: '/otaUpdates' - } - ] - }, - { - name: 'entity-view.management', - places: [ - { - name: 'entity-view.entity-views', - icon: 'view_quilt', - path: '/entityViews' - } - ] - } - ); - if (authState.edgesSupportEnabled) { - homeSections.push( - { - name: 'edge.management', - places: [ - { - name: 'edge.edge-instances', - icon: 'router', - path: '/edgeInstances' - }, - { - name: 'edge.rulechain-templates', - icon: 'settings_ethernet', - path: '/edgeManagement/ruleChains' - } - ] - } - ); - } - homeSections.push( - { - name: 'dashboard.management', - places: [ - { - name: 'widget.widget-library', - icon: 'now_widgets', - path: '/widgets-bundles' - }, - { - name: 'dashboard.dashboards', - icon: 'dashboard', - path: '/dashboards' - } - ] - }, - { - name: 'version-control.management', - places: [ - { - name: 'version-control.version-control', - icon: 'history', - path: '/vc' - } - ] - }, - { - name: 'audit-log.audit', - places: [ - { - name: 'audit-log.audit-logs', - icon: 'track_changes', - path: '/auditLogs' - }, - { - name: 'api-usage.api-usage', - icon: 'insert_chart', - path: '/usage' - } - ] - }, - { - name: 'admin.system-settings', - places: [ - { - name: 'admin.home-settings', - icon: 'settings_applications', - path: '/settings/home' - }, - { - name: 'resource.resources-library', - icon: 'folder', - path: '/settings/resources-library' - }, - { - name: 'admin.repository-settings', - icon: 'manage_history', - path: '/settings/repository', - }, - { - name: 'admin.auto-commit-settings', - icon: 'settings_backup_restore', - path: '/settings/auto-commit' - } - ] - } - ); - return homeSections; - } - - private buildCustomerUserHome(authState: AuthState): Array { - const homeSections: Array = []; - homeSections.push( - { - name: 'asset.view-assets', - places: [ - { - name: 'asset.assets', - icon: 'domain', - path: '/assets' - } - ] - }, - { - name: 'device.view-devices', - places: [ - { - name: 'device.devices', - icon: 'devices_other', - path: '/devices' - } - ] - }, - { - name: 'entity-view.management', - places: [ - { - name: 'entity-view.entity-views', - icon: 'view_quilt', - path: '/entityViews' - } - ] - } - ); - if (authState.edgesSupportEnabled) { - homeSections.push( - { - name: 'edge.management', - places: [ - { - name: 'edge.edge-instances', - icon: 'settings_input_antenna', - path: '/edgeInstances' - } - ] - } - ); - } - homeSections.push( - { - name: 'dashboard.view-dashboards', - places: [ - { - name: 'dashboard.dashboards', - icon: 'dashboard', - path: '/dashboards' - } - ] - } - ); - return homeSections; - } - private allMenuLinks(sections: Array): Array { const result: Array = []; for (const section of sections) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html index 72b154d89a..0d59ed8564 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html @@ -26,7 +26,7 @@ {{place.icon}} - {{place.name}} + {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts index 8ff8fa74e9..539b77f34a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts @@ -20,7 +20,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { MenuService } from '@core/services/menu.service'; -import { HomeSection, HomeSectionPlace } from '@core/services/menu.models'; +import { HomeSection, MenuSection } from '@core/services/menu.models'; import { Router } from '@angular/router'; import { map } from 'rxjs/operators'; @@ -38,9 +38,7 @@ export class NavigationCardsWidgetComponent extends PageComponent implements OnI homeSections$ = this.menuService.homeSections(); showHomeSections$ = this.homeSections$.pipe( - map((sections) => { - return sections.filter((section) => this.sectionPlaces(section).length > 0); - }) + map((sections) => sections.filter((section) => this.sectionPlaces(section).length > 0)) ); cols = null; @@ -85,11 +83,11 @@ export class NavigationCardsWidgetComponent extends PageComponent implements OnI }); } - sectionPlaces(section: HomeSection): HomeSectionPlace[] { + sectionPlaces(section: HomeSection): MenuSection[] { return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : []; } - private filterPlace(place: HomeSectionPlace): boolean { + private filterPlace(place: MenuSection): boolean { if (this.settings.filterType === 'include') { return this.settings.filter.includes(place.path); } else if (this.settings.filterType === 'exclude') { diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html index 132d0e2c65..81142c501f 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html @@ -28,7 +28,7 @@ {{place.icon}} - {{place.name}} + {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} From 83cf86f6df7fac792efb115c8c46e4bbc3fad51b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 20 Aug 2024 14:51:51 +0300 Subject: [PATCH 252/331] deleted redundant validation --- .../server/dao/domain/DomainServiceImpl.java | 3 --- .../server/dao/mobile/MobileAppServiceImpl.java | 4 ---- .../dao/oauth2/OAuth2ClientServiceImpl.java | 16 ---------------- 3 files changed, 23 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java index 487e3feb5c..b3380ef540 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java @@ -42,8 +42,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import static org.thingsboard.server.dao.service.Validator.validateId; - @Slf4j @Service public class DomainServiceImpl extends AbstractEntityService implements DomainService { @@ -128,7 +126,6 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe @Override public void deleteDomainsByTenantId(TenantId tenantId) { log.trace("Executing deleteDomainsByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); domainDao.deleteByTenantId(tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index f5b558cdcc..1e89b3c645 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -35,7 +35,6 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; -import org.thingsboard.server.dao.service.Validator; import java.util.List; import java.util.Map; @@ -47,8 +46,6 @@ import java.util.stream.Collectors; @Service public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService { - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - @Autowired private OAuth2ClientDao oauth2ClientDao; @Autowired @@ -135,7 +132,6 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil @Override public void deleteMobileAppsByTenantId(TenantId tenantId) { log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); mobileAppDao.deleteByTenantId(tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index 6273ee2763..a39416620e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -34,25 +34,16 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.service.Validator; import java.util.List; import java.util.Optional; -import java.util.UUID; import java.util.stream.Collectors; -import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; -import static org.thingsboard.server.dao.service.Validator.validateString; @Slf4j @Service("OAuth2ClientService") public class OAuth2ClientServiceImpl extends AbstractEntityService implements OAuth2ClientService { - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_CLIENT_REGISTRATION_ID = "Incorrect clientRegistrationId "; - public static final String INCORRECT_DOMAIN_NAME = "Incorrect domainName "; - @Autowired private OAuth2ClientDao oauth2ClientDao; @Autowired @@ -61,7 +52,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public List findOAuth2ClientLoginInfosByDomainName(String domainName) { log.trace("Executing findOAuth2ClientLoginInfosByDomainName [{}] ", domainName); - validateString(domainName, dn -> INCORRECT_DOMAIN_NAME + dn); return oauth2ClientDao.findEnabledByDomainName(domainName) .stream() .map(OAuth2Utils::toClientLoginInfo) @@ -90,7 +80,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public OAuth2Client findOAuth2ClientById(TenantId tenantId, OAuth2ClientId oAuth2ClientId) { log.trace("Executing findOAuth2ClientById [{}]", oAuth2ClientId); - validateId(oAuth2ClientId, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); return oauth2ClientDao.findById(tenantId, oAuth2ClientId.getId()); } @@ -103,8 +92,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public String findAppSecret(OAuth2ClientId oAuth2ClientId, String pkgName) { log.trace("Executing findAppSecret [{}][{}]", oAuth2ClientId, pkgName); - validateId(oAuth2ClientId, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); - validateString(pkgName, "Incorrect package name"); return oauth2ClientDao.findAppSecret(oAuth2ClientId.getId(), pkgName); } @@ -123,7 +110,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public void deleteOauth2ClientsByTenantId(TenantId tenantId) { log.trace("Executing deleteOauth2ClientsByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); oauth2ClientDao.deleteByTenantId(tenantId.getId()); } @@ -137,8 +123,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public List findOAuth2ClientInfosByIds(TenantId tenantId, List oAuth2ClientIds) { log.trace("Executing findQueueStatsByIds, tenantId [{}], queueStatsIds [{}]", tenantId, oAuth2ClientIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(oAuth2ClientIds, ids -> "Incorrect clientIds " + ids); return oauth2ClientDao.findByIds(tenantId, oAuth2ClientIds) .stream() .map(OAuth2ClientInfo::new) From bf0d4685105816f38cb4beba714928f0c6907958 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 15:31:43 +0300 Subject: [PATCH 253/331] UI: Breadcrumb improvements. --- ui-ngx/src/app/core/services/menu.models.ts | 16 ++++++++++++++-- .../shared/components/breadcrumb.component.ts | 11 ++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index e27d015fbe..0d74e37c5f 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -92,6 +92,7 @@ export enum MenuId { rule_chains = 'rule_chains', edge_management = 'edge_management', edges = 'edges', + edge_instances = 'edge_instances', rulechain_templates = 'rulechain_templates', features = 'features', otaUpdates = 'otaUpdates', @@ -101,7 +102,7 @@ export enum MenuId { declare type MenuFilter = (authState: AuthState) => boolean; -const menuSectionMap = new Map([ +export const menuSectionMap = new Map([ [ MenuId.home, { @@ -545,6 +546,17 @@ const menuSectionMap = new Map([ icon: 'router' } ], + [ + MenuId.edge_instances, + { + id: MenuId.edge_instances, + name: 'edge.edge-instances', + fullName: 'edge.edge-instances', + type: 'link', + path: '/edgeManagement/instances', + icon: 'router' + } + ], [ MenuId.rulechain_templates, { @@ -756,7 +768,7 @@ const defaultUserMenuMap = new Map([ {id: MenuId.entity_views} ] }, - {id: MenuId.edges}, + {id: MenuId.edge_instances}, { id: MenuId.notifications_center, pages: [ diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 23c9abe337..c0899d7ef7 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -24,7 +24,7 @@ import { guid } from '@core/utils'; import { BroadcastService } from '@core/services/broadcast.service'; import { ActiveComponentService } from '@core/services/active-component.service'; import { UtilsService } from '@core/services/utils.service'; -import { MenuSection } from '@core/services/menu.models'; +import { MenuSection, menuSectionMap } from '@core/services/menu.models'; import { MenuService } from '@core/services/menu.service'; @Component({ @@ -113,8 +113,13 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; if (breadcrumbConfig && !breadcrumbConfig.skip) { let labelFunction: () => string; - const section: MenuSection = breadcrumbConfig.menuId ? - availableMenuSections.find(menu => menu.id === breadcrumbConfig.menuId) : null; + let section: MenuSection = null; + if (breadcrumbConfig.menuId) { + section = availableMenuSections.find(menu => menu.id === breadcrumbConfig.menuId); + if (!section) { + section = menuSectionMap.get(breadcrumbConfig.menuId); + } + } const label = section?.name || breadcrumbConfig.label || 'home.home'; const customTranslate = section?.customTranslate || false; if (breadcrumbConfig.labelFunction) { From 7e9950c9f5c2d229f8a8dba05123038ccea0d7d8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 20 Aug 2024 15:38:50 +0300 Subject: [PATCH 254/331] Minor improvements --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 0d74e37c5f..d11b28a97f 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -860,7 +860,7 @@ const defaultHomeSectionMap = new Map([ }, { name: 'edge.management', - places: [MenuId.edges] + places: [MenuId.edge_instances] }, { name: 'dashboard.view-dashboards', From 5032952800b7332c1aaa2b0844d802ad8a1f0760 Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 20 Aug 2024 17:43:44 +0300 Subject: [PATCH 255/331] UI: fixed menu for OAuth --- ui-ngx/src/app/core/services/menu.models.ts | 45 ++++++++++++++++++- .../admin/oauth2/oauth2-routing.module.ts | 10 ++--- .../assets/locale/locale.constant-en_US.json | 3 +- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index d11b28a97f..29fb95ca1d 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -78,6 +78,9 @@ export enum MenuId { security_settings_general = 'security_settings_general', two_fa = '2fa', oauth2 = 'oauth2', + domains = 'domains', + mobile_apps = 'mobile_apps', + clients = 'clients', audit_log = 'audit_log', alarms = 'alarms', dashboards = 'dashboards', @@ -405,6 +408,36 @@ export const menuSectionMap = new Map([ icon: 'mdi:shield-account' } ], + [ + MenuId.domains, + { + id: MenuId.domains, + name: 'admin.oauth2.domains', + type: 'link', + path: '/security-settings/oauth2/domains', + icon: 'domain' + } + ], + [ + MenuId.mobile_apps, + { + id: MenuId.mobile_apps, + name: 'admin.oauth2.mobile-apps', + type: 'link', + path: '/security-settings/oauth2/mobile-applications', + icon: 'smartphone' + } + ], + [ + MenuId.clients, + { + id: MenuId.clients, + name: 'admin.oauth2.clients', + type: 'link', + path: '/security-settings/oauth2/clients', + icon: 'public' + } + ], [ MenuId.audit_log, { @@ -669,7 +702,14 @@ const defaultUserMenuMap = new Map([ pages: [ {id: MenuId.security_settings_general}, {id: MenuId.two_fa}, - {id: MenuId.oauth2} + { + id: MenuId.oauth2, + pages: [ + {id: MenuId.domains}, + {id: MenuId.mobile_apps}, + {id: MenuId.clients} + ] + } ] } ] @@ -794,7 +834,8 @@ const defaultHomeSectionMap = new Map([ { name: 'admin.system-settings', places: [MenuId.general, MenuId.mail_server, - MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.two_fa, MenuId.resources_library, MenuId.queues] + MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.domains, MenuId.mobile_apps, + MenuId.clients, MenuId.two_fa, MenuId.resources_library, MenuId.queues] } ] ], diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts index b1e18ed502..22c172ca14 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts @@ -27,6 +27,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { MobileAppTableConfigResolver } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver'; +import { MenuId } from '@core/services/menu.models'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -66,8 +67,7 @@ export const oAuth2Routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.oauth2.domains', breadcrumb: { - label: 'admin.oauth2.domains', - icon: 'domain' + menuId: MenuId.domains } }, resolve: { @@ -81,8 +81,7 @@ export const oAuth2Routes: Routes = [ auth: [Authority.SYS_ADMIN], title: 'admin.oauth2.mobile-apps', breadcrumb: { - label: 'admin.oauth2.mobile-apps', - icon: 'smartphone' + menuId: MenuId.mobile_apps } }, resolve: { @@ -94,8 +93,7 @@ export const oAuth2Routes: Routes = [ data: { title: 'admin.oauth2.clients', breadcrumb: { - label: 'admin.oauth2.clients', - icon: 'public' + menuId:MenuId.clients } }, children: [ 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 1df3e0933f..0b1531daf2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -223,7 +223,7 @@ "authorization-uri": "Authorization URI", "authorization-uri-required": "Authorization URI is required.", "add-client": "Add OAuth 2.0 client", - "client-details": "OAuth 2.0 details", + "client-details": "OAuth 2.0 client details", "client": "OAuth 2.0 client", "clients": "OAuth 2.0 clients", "no-oauth2-clients": "No OAuth 2.0 clients found", @@ -2360,7 +2360,6 @@ "type-notification-templates": "Notification templates", "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", "link": "link", - "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", "type-oauth2-client": "OAuth 2.0 client", "type-oauth2-clients": "OAuth 2.0 clients", "list-of-oauth2-clients": "{ count, plural, =1 {One OAuth 2.0 client} other {List of # OAuth 2.0 clients} }", From b5be7af40f23c66ee75cdfd5c4fb970b92fb0843 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 21 Aug 2024 12:07:20 +0300 Subject: [PATCH 256/331] Fixed SYNC issue with redundant name fields in server and broker config --- .../broker-config-control.component.ts | 5 +---- .../opc-server-config/opc-server-config.component.ts | 1 - .../lib/gateway/gateway-connectors.component.ts | 11 ----------- .../metadata/connector-default-configs/mqtt.json | 1 - .../metadata/connector-default-configs/opcua.json | 1 - 5 files changed, 1 insertion(+), 18 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index d4bfdd4694..e44506dccc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -33,7 +33,6 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TranslateService } from '@ngx-translate/core'; import { generateSecret } from '@core/utils'; import { Subject } from 'rxjs'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; @@ -74,10 +73,8 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid private destroy$ = new Subject(); constructor(private fb: FormBuilder, - private cdr: ChangeDetectorRef, - private translate: TranslateService) { + private cdr: ChangeDetectorRef) { this.brokerConfigFormGroup = this.fb.group({ - name: ['', []], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], version: [5, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 5b14f16db2..5b63689c3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -77,7 +77,6 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator constructor(private fb: FormBuilder) { this.serverConfigFormGroup = this.fb.group({ - name: ['', []], url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]], scanPeriodInMillis: [1000, [Validators.required, Validators.min(1000)]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index e7e4d05e45..2915cb831e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -147,8 +147,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } ngAfterViewInit(): void { - this.observeName(); - this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = this.getSortingDataAccessor(); @@ -533,15 +531,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.connectorForm.disable(); } - private observeName(): void { - this.connectorForm.get('name').valueChanges - .pipe( - filter(() => this.connectorForm.get('type').value === ConnectorType.MQTT), - takeUntil(this.destroy$) - ) - .subscribe(name => this.connectorForm.get('basicConfig').get('broker.name')?.setValue(name)); - } - private getSortingDataAccessor(): (data: GatewayAttributeData, sortHeaderId: string) => string | number { return (data: GatewayAttributeData, sortHeaderId: string) => { switch (sortHeaderId) { diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json index c60e1dd956..3e0800c5d6 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json @@ -1,6 +1,5 @@ { "broker": { - "name": "Default Local Broker", "host": "127.0.0.1", "port": 1883, "clientId": "ThingsBoard_gateway", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json index 97621deda8..2e93f33942 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json @@ -1,6 +1,5 @@ { "server": { - "name": "OPC-UA Default Server", "url": "localhost:4840/freeopcua/server/", "timeoutInMillis": 5000, "scanPeriodInMillis": 5000, From 7da08551806b3c9ec15ac58afb8f738eb2af2a38 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 21 Aug 2024 12:10:07 +0300 Subject: [PATCH 257/331] renamed enum --- ...angeOriginatorSource.java => OriginatorSource.java} | 2 +- .../rule/engine/transform/TbChangeOriginatorNode.java | 4 ++-- .../transform/TbChangeOriginatorNodeConfiguration.java | 4 ++-- .../engine/transform/TbChangeOriginatorNodeTest.java | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) rename rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/{ChangeOriginatorSource.java => OriginatorSource.java} (95%) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/ChangeOriginatorSource.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/OriginatorSource.java similarity index 95% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/ChangeOriginatorSource.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/OriginatorSource.java index 57b3ded5fa..2ae5402404 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/ChangeOriginatorSource.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/OriginatorSource.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.transform; -public enum ChangeOriginatorSource { +public enum OriginatorSource { CUSTOMER, TENANT, RELATED, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 522756c880..d590bbbb75 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -36,8 +36,8 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; import java.util.NoSuchElementException; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.ENTITY; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.RELATED; +import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY; +import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED; @Slf4j @RuleNode( diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java index 8fcf2e8ce1..76d42c2ef0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java @@ -24,12 +24,12 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.Collections; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.CUSTOMER; +import static org.thingsboard.rule.engine.transform.OriginatorSource.CUSTOMER; @Data public class TbChangeOriginatorNodeConfiguration implements NodeConfiguration { - private ChangeOriginatorSource originatorSource; + private OriginatorSource originatorSource; private RelationsQuery relationsQuery; private String entityType; private String entityNamePattern; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index 9334f9ccb1..098fe3efda 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -69,11 +69,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.ALARM_ORIGINATOR; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.CUSTOMER; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.ENTITY; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.RELATED; -import static org.thingsboard.rule.engine.transform.ChangeOriginatorSource.TENANT; +import static org.thingsboard.rule.engine.transform.OriginatorSource.ALARM_ORIGINATOR; +import static org.thingsboard.rule.engine.transform.OriginatorSource.CUSTOMER; +import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY; +import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED; +import static org.thingsboard.rule.engine.transform.OriginatorSource.TENANT; @ExtendWith(MockitoExtension.class) public class TbChangeOriginatorNodeTest { From 3db5278e05457a6e70c1aa4c6f33338ae9b9c60b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 21 Aug 2024 12:23:18 +0300 Subject: [PATCH 258/331] added generic permission for get all endpoints --- .../server/controller/DomainController.java | 1 + .../controller/MobileAppController.java | 1 + .../server/controller/OAuth2Controller.java | 4 +- .../mobile/DefaultTbMobileAppService.java | 4 +- .../DefaultSystemDataLoaderService.java | 10 ++--- .../HybridClientRegistrationRepository.java | 6 +-- .../dao/oauth2/OAuth2ClientServiceImpl.java | 4 +- .../main/resources/sql/schema-entities.sql | 43 ------------------- 8 files changed, 17 insertions(+), 56 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/DomainController.java b/application/src/main/java/org/thingsboard/server/controller/DomainController.java index fa1f639281..41eea463df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DomainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DomainController.java @@ -106,6 +106,7 @@ public class DomainController extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.DOMAIN, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return domainService.findDomainInfosByTenantId(getTenantId(), pageLink); } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 6620e854d0..fba17d7119 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -107,6 +107,7 @@ public class MobileAppController extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink); } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 0eddf8332b..261c4705a9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -127,6 +127,7 @@ public class OAuth2Controller extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } @@ -166,7 +167,8 @@ public class OAuth2Controller extends BaseController { "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/oauth2/loginProcessingUrl") - public String getLoginProcessingUrl() { + public String getLoginProcessingUrl() throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java index 7d5f1122da..66da7341cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java @@ -60,9 +60,9 @@ public class DefaultTbMobileAppService extends AbstractTbEntityService implement MobileAppId mobileAppId = mobileApp.getId(); try { mobileAppService.updateOauth2Clients(tenantId, mobileAppId, oAuth2ClientIds); - logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, oAuth2ClientIds.toString()); + logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, oAuth2ClientIds); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e, oAuth2ClientIds.toString()); + logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e, oAuth2ClientIds); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index abab7b1910..52ef9b203f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -311,14 +311,14 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { List mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData(); if (CollectionUtils.isNotEmpty(mobiles)) { mobiles.stream() - .filter(config -> !validateKeyLength(config.getAppSecret())) - .forEach(config -> { + .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret())) + .forEach(mobileApp -> { log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + "A new Application Secret has been added automatically for Mobile Application [{}]. " + "You can change the Application Secret using the Web UI: " + - "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", config.getPkgName()); - config.setAppSecret(generateRandomKey()); - mobileAppDao.save(TenantId.SYS_TENANT_ID, config); + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); + mobileApp.setAppSecret(generateRandomKey()); + mobileAppDao.save(TenantId.SYS_TENANT_ID, mobileApp); }); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 8009cf98f6..c23ab1e3d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -36,9 +36,9 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep @Override public ClientRegistration findByRegistrationId(String registrationId) { - OAuth2Client registration = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(registrationId))); - return registration == null ? - null : toSpringClientRegistration(registration); + OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(registrationId))); + return oAuth2Client == null ? + null : toSpringClientRegistration(oAuth2Client); } private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index a39416620e..af0a881e49 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -91,7 +91,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public String findAppSecret(OAuth2ClientId oAuth2ClientId, String pkgName) { - log.trace("Executing findAppSecret [{}][{}]", oAuth2ClientId, pkgName); + log.trace("Executing findAppSecret oAuth2ClientId = [{}] pkgName = [{}]", oAuth2ClientId, pkgName); return oauth2ClientDao.findAppSecret(oAuth2ClientId.getId(), pkgName); } @@ -122,7 +122,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public List findOAuth2ClientInfosByIds(TenantId tenantId, List oAuth2ClientIds) { - log.trace("Executing findQueueStatsByIds, tenantId [{}], queueStatsIds [{}]", tenantId, oAuth2ClientIds); + log.trace("Executing findQueueStatsByIds, tenantId [{}], oAuth2ClientIds [{}]", tenantId, oAuth2ClientIds); return oauth2ClientDao.findByIds(tenantId, oAuth2ClientIds) .stream() .map(OAuth2ClientInfo::new) diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 7ce086f3e5..46e739e5d5 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -677,49 +677,6 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) ); --- Deprecated -CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( - id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, - enabled boolean, - created_time bigint NOT NULL, - additional_info varchar, - client_id varchar(255), - client_secret varchar(255), - authorization_uri varchar(255), - token_uri varchar(255), - scope varchar(255), - user_info_uri varchar(255), - user_name_attribute_name varchar(255), - jwk_set_uri varchar(255), - client_authentication_method varchar(255), - login_button_label varchar(255), - login_button_icon varchar(255), - allow_user_creation boolean, - activate_user boolean, - type varchar(31), - basic_email_attribute_key varchar(31), - basic_first_name_attribute_key varchar(31), - basic_last_name_attribute_key varchar(31), - basic_tenant_name_strategy varchar(31), - basic_tenant_name_pattern varchar(255), - basic_customer_name_pattern varchar(255), - basic_default_dashboard_name varchar(255), - basic_always_full_screen boolean, - custom_url varchar(255), - custom_username varchar(255), - custom_password varchar(255), - custom_send_token boolean -); - --- Deprecated -CREATE TABLE IF NOT EXISTS oauth2_client_registration ( - id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, - created_time bigint NOT NULL, - domain_name varchar(255), - domain_scheme varchar(31), - client_registration_info_id uuid -); - CREATE TABLE IF NOT EXISTS api_usage_state ( id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY, created_time bigint NOT NULL, From 92c09243ef049328065fdb6eaa3d78d3f4aaa345 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 21 Aug 2024 12:42:20 +0300 Subject: [PATCH 259/331] Fix transactionalCache.getOrFetchFromDB --- .../org/thingsboard/server/cache/TbTransactionalCache.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index 89e6754d19..765c82a8db 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -53,7 +53,7 @@ public interface TbTransactionalCache cacheValueWrapper = get(key); + TbCacheValueWrapper cacheValueWrapper = get(key, true); if (cacheValueWrapper != null) { return cacheValueWrapper.get(); } @@ -92,7 +92,7 @@ public interface TbTransactionalCache cacheValueWrapper = get(key); + TbCacheValueWrapper cacheValueWrapper = get(key, true); if (cacheValueWrapper != null) { var cacheValue = cacheValueWrapper.get(); return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); From dba36a8e5a31132e43ced5993d5069d3ca4cbfb0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 21 Aug 2024 13:46:48 +0300 Subject: [PATCH 260/331] fixed upgrade script --- .../src/main/data/upgrade/3.7.0/schema_update.sql | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index b0856c2de8..d400821fa3 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -104,7 +104,6 @@ ALTER TABLE tenant ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; ALTER TABLE IF EXISTS oauth2_mobile RENAME TO mobile_app; ALTER TABLE IF EXISTS oauth2_domain RENAME TO domain; -ALTER TABLE IF EXISTS oauth2_domain RENAME domain_name TO name; ALTER TABLE IF EXISTS oauth2_registration RENAME TO oauth2_client; ALTER TABLE domain ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, @@ -112,6 +111,16 @@ ALTER TABLE domain ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080', DROP COLUMN IF EXISTS domain_scheme; +-- rename column domain_name to name +DO +$$ + BEGIN + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='domain' and column_name='domain_name') THEN + ALTER TABLE domain RENAME COLUMN domain_name TO name; + END IF; + END +$$; + -- delete duplicated domains DELETE FROM domain d1 USING ( SELECT MIN(ctid) as ctid, name From 3eb760e64806fec92f7128495a71148122455ea2 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 21 Aug 2024 13:52:22 +0200 Subject: [PATCH 261/331] fixed queries for get/count devices without ota --- .../server/dao/sql/device/DeviceRepository.java | 14 ++++---------- .../server/dao/sql/device/JpaDeviceDao.java | 5 ++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 289da44626..c55210b606 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -83,33 +83,27 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + - "AND d.firmwareId = null " + - "AND (:textSearch IS NULL OR ilike(d.name, CONCAT('%', :textSearch, '%')) = true " + - "OR ilike(d.label, CONCAT('%', :textSearch, '%')) = true)") + "AND d.firmwareId IS NULL") Page findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, - @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + - "AND d.softwareId = null " + - "AND (:textSearch IS NULL OR ilike(d.name, CONCAT('%', :textSearch, '%')) = true " + - "OR ilike(d.label, CONCAT('%', :textSearch, '%')) = true)") + "AND d.softwareId IS NULL") Page findByTenantIdAndTypeAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, - @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + - "AND d.firmwareId = null") + "AND d.firmwareId IS NULL") Long countByTenantIdAndDeviceProfileIdAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId); @Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + - "AND d.softwareId = null") + "AND d.softwareId IS NULL") Long countByTenantIdAndDeviceProfileIdAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index f6ff3c8915..48bb998016 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -179,10 +179,9 @@ public class JpaDeviceDao extends JpaAbstractDao implement OtaPackageType type, PageLink pageLink) { Pageable pageable = DaoUtil.toPageable(pageLink); - String searchText = pageLink.getTextSearch(); Page page = OtaPackageUtil.getByOtaPackageType( - () -> deviceRepository.findByTenantIdAndTypeAndFirmwareIdIsNull(tenantId, deviceProfileId, searchText, pageable), - () -> deviceRepository.findByTenantIdAndTypeAndSoftwareIdIsNull(tenantId, deviceProfileId, searchText, pageable), + () -> deviceRepository.findByTenantIdAndTypeAndFirmwareIdIsNull(tenantId, deviceProfileId, pageable), + () -> deviceRepository.findByTenantIdAndTypeAndSoftwareIdIsNull(tenantId, deviceProfileId, pageable), type ); return DaoUtil.toPageData(page); From ef93522df08e214d4336a09a092d8d01e90afc87 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 21 Aug 2024 15:56:24 +0300 Subject: [PATCH 262/331] UI: Fix MenuId enum 2fa -> two_fa --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index d11b28a97f..28ebbfaf79 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -76,7 +76,7 @@ export enum MenuId { mobile_app_settings = 'mobile_app_settings', security_settings = 'security_settings', security_settings_general = 'security_settings_general', - two_fa = '2fa', + two_fa = 'two_fa', oauth2 = 'oauth2', audit_log = 'audit_log', alarms = 'alarms', From 7f2d78cb04df0d69570ca3a4623681e883bc504a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 21 Aug 2024 15:28:18 +0200 Subject: [PATCH 263/331] added corresponding tests --- .../server/dao/device/DeviceService.java | 2 +- .../server/common/data/HasOtaPackage.java | 4 + .../server/dao/device/DeviceServiceImpl.java | 2 +- .../server/dao/service/DeviceServiceTest.java | 125 ++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 8a1f1cb5a8..005c740571 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -77,7 +77,7 @@ public interface DeviceService extends EntityDaoService { PageData findDevicesByTenantIdAndTypeAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType type, PageLink pageLink); - Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType); + long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType); ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasOtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasOtaPackage.java index 1b62f4c36a..93a170ef94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/HasOtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasOtaPackage.java @@ -22,4 +22,8 @@ public interface HasOtaPackage { OtaPackageId getFirmwareId(); OtaPackageId getSoftwareId(); + + void setFirmwareId(OtaPackageId otaPackageId); + + void setSoftwareId(OtaPackageId otaPackageId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 2c80354923..e6ba0e5e72 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -407,7 +407,7 @@ public class DeviceServiceImpl extends CachedVersionedEntityService INCORRECT_TENANT_ID + id); validateId(deviceProfileId, id -> INCORRECT_DEVICE_PROFILE_ID + id); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index a54399e85d..9fe3f85463 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -33,13 +33,18 @@ import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.HasOtaPackage; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -63,6 +68,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; +import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @DaoSqlTest @@ -202,6 +208,125 @@ public class DeviceServiceTest extends AbstractServiceTest { deleteDevice(anotherTenantId, anotherDevice); } + @Test + public void testCountDevicesWithoutFirmware() { + testCountDevicesWithoutOta(FIRMWARE); + } + + @Test + public void testCountDevicesWithoutSoftware() { + testCountDevicesWithoutOta(SOFTWARE); + } + + public void testCountDevicesWithoutOta(OtaPackageType type) { + var defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + var deviceProfileId = defaultDeviceProfile.getId(); + Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); + Assert.assertEquals(0, deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId, deviceProfileId, type)); + + int maxDevices = 8; + List devices = new ArrayList<>(maxDevices); + + for (int i = 1; i <= maxDevices; i++) { + devices.add(this.saveDevice(tenantId, "My device " + i)); + Assert.assertEquals(i, deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId, deviceProfileId, type)); + } + + Assert.assertEquals(maxDevices, deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId, deviceProfileId, type)); + + var otaPackageId = createOta(deviceProfileId, type); + + int devicesWithOta = maxDevices / 2; + + for (int i = 0; i < devicesWithOta; i++) { + var device = devices.get(i); + setOtaPackageId(device, type, otaPackageId); + deviceService.saveDevice(device); + } + + Assert.assertEquals(maxDevices - devicesWithOta, deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId, deviceProfileId, type)); + + devices.forEach(device -> deleteDevice(tenantId, device)); + } + + @Test + public void testFindDevicesWithoutFirmware() { + testFindDevicesWithoutOta(FIRMWARE); + } + + @Test + public void testFindDevicesWithoutSoftware() { + testFindDevicesWithoutOta(SOFTWARE); + } + + public void testFindDevicesWithoutOta(OtaPackageType type) { + var defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + var deviceProfileId = defaultDeviceProfile.getId(); + + PageLink pageLink = new PageLink(100); + + Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); + Assert.assertEquals(0, deviceService.findDevicesByTenantIdAndTypeAndEmptyOtaPackage(tenantId, deviceProfileId, type, pageLink).getData().size()); + + int maxDevices = 8; + List devices = new ArrayList<>(maxDevices); + + for (int i = 1; i <= maxDevices; i++) { + devices.add(this.saveDevice(tenantId, "My device " + i)); + } + + var foundDevices = deviceService.findDevicesByTenantIdAndTypeAndEmptyOtaPackage(tenantId, deviceProfileId, type, pageLink).getData(); + Assert.assertEquals(maxDevices, foundDevices.size()); + + devices.sort(idComparator); + foundDevices.sort(idComparator); + + Assert.assertEquals(devices, foundDevices); + + var otaPackageId = createOta(deviceProfileId, type); + + int devicesWithOta = maxDevices / 2; + + for (int i = 0; i < devicesWithOta; i++) { + var device = devices.get(i); + setOtaPackageId(device, type, otaPackageId); + deviceService.saveDevice(device); + } + + foundDevices = deviceService.findDevicesByTenantIdAndTypeAndEmptyOtaPackage(tenantId, deviceProfileId, type, pageLink).getData(); + + Assert.assertEquals(maxDevices - devicesWithOta, foundDevices.size()); + + foundDevices.sort(idComparator); + + for (int i = 0; i < foundDevices.size(); i++) { + Assert.assertEquals(devices.get(i + devicesWithOta), foundDevices.get(i)); + } + + devices.forEach(device -> deleteDevice(tenantId, device)); + } + + private void setOtaPackageId(T obj, OtaPackageType type, OtaPackageId otaPackageId) { + switch (type) { + case FIRMWARE -> obj.setFirmwareId(otaPackageId); + case SOFTWARE -> obj.setSoftwareId(otaPackageId); + } + } + + private OtaPackageId createOta(DeviceProfileId deviceProfileId, OtaPackageType type) { + OtaPackageInfo ota = new OtaPackageInfo(); + ota.setTenantId(tenantId); + ota.setDeviceProfileId(deviceProfileId); + ota.setType(type); + ota.setTitle("Test_" + type); + ota.setVersion("v1.0"); + ota.setUrl("http://ota.test.org"); + ota.setDataSize(0L); + OtaPackageInfo savedOta = otaPackageService.saveOtaPackageInfo(ota, true); + Assert.assertNotNull(savedOta); + return savedOta.getId(); + } + void deleteDevice(TenantId tenantId, Device device) { deviceService.deleteDevice(tenantId, device.getId()); } From 3eb7efe0adf715cc3bdac4510737bae7c60d7861 Mon Sep 17 00:00:00 2001 From: rusikv Date: Wed, 21 Aug 2024 16:31:07 +0300 Subject: [PATCH 264/331] UI: fixed oauth2 client lists not updating with values --- .../oauth2/clients/clients-table-config.resolver.ts | 2 +- .../oauth2/domains/domain-table-config.resolver.ts | 13 ++++++++++--- .../pages/admin/oauth2/domains/domain.component.ts | 4 ++-- .../mobile-apps/mobile-app-table-config.resolver.ts | 11 +++++++++-- .../oauth2/mobile-apps/mobile-app.component.ts | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts index 4fab82971f..d1d760d908 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts @@ -81,7 +81,7 @@ export class ClientsTableConfigResolver implements Resolve this.oauth2Service.findTenantOAuth2ClientInfos(pageLink); this.config.loadEntity = id => this.oauth2Service.getOAuth2ClientById(id.id); this.config.saveEntity = client => { - return this.oauth2Service.saveOAuth2Client(client as OAuth2Client); + return this.oauth2Service.saveOAuth2Client(client); } this.config.deleteEntity = id => this.oauth2Service.deleteOauth2Client(id.id); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts index a3cc535d62..4bccc41dc7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts @@ -23,7 +23,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { Domain, DomainInfo } from '@shared/models/oauth2.models'; +import { DomainInfo } from '@shared/models/oauth2.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; @@ -33,6 +33,7 @@ import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.compone import { isEqual } from '@core/utils'; import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; import { Direction } from '@app/shared/models/page/sort-order'; +import { map } from 'rxjs'; @Injectable() export class DomainTableConfigResolver implements Resolve> { @@ -88,11 +89,17 @@ export class DomainTableConfigResolver implements Resolve this.domainService.getDomainInfoById(id.id); this.config.saveEntity = (domain, originalDomain) => { const clientsIds = domain.oauth2ClientInfos as Array || []; - if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), originalDomain.oauth2ClientInfos?.map(info => info.id.id).sort())) { + if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), + originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) { this.domainService.updateOauth2Clients(domain.id.id, clientsIds).subscribe(); } delete domain.oauth2ClientInfos; - return this.domainService.saveDomain(domain as Domain, domain.id ? [] : clientsIds); + return this.domainService.saveDomain(domain, domain.id ? [] : clientsIds).pipe( + map(domain => { + (domain as DomainInfo).oauth2ClientInfos = clientsIds; + return domain; + }) + ); } this.config.deleteEntity = id => this.domainService.deleteDomain(id.id); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts index 853bf17e06..d54c969bfe 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts @@ -73,9 +73,9 @@ export class DomainComponent extends EntityComponent { this.entityForm.patchValue({ name: entity.name, oauth2Enabled: entity.oauth2Enabled, - oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id.id), + oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info), propagateToEdge: entity.propagateToEdge - }) + }); } redirectURI(): string { diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts index b4c6c7576d..7585cfff46 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts @@ -34,6 +34,7 @@ import { Direction } from '@app/shared/models/page/sort-order'; import { MobileAppService } from '@core/http/mobile-app.service'; import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; +import { map } from 'rxjs'; @Injectable() export class MobileAppTableConfigResolver implements Resolve> { @@ -94,11 +95,17 @@ export class MobileAppTableConfigResolver implements Resolve this.mobileAppService.getMobileAppInfoById(id.id); this.config.saveEntity = (mobileApp, originalMobileApp) => { const clientsIds = mobileApp.oauth2ClientInfos as Array || []; - if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), originalMobileApp.oauth2ClientInfos?.map(info => info.id.id).sort())) { + if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), + originalMobileApp.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) { this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds).subscribe(); } delete mobileApp.oauth2ClientInfos; - return this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds); + return this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds).pipe( + map(mobileApp => { + (mobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds; + return mobileApp; + }) + ); } this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts index 0baa72f5f7..2cca292fc3 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts @@ -67,7 +67,7 @@ export class MobileAppComponent extends EntityComponent { pkgName: entity.pkgName, appSecret: entity.appSecret, oauth2Enabled: entity.oauth2Enabled, - oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id.id) + oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info) }) } From 45f298fb0d9e1e83636bfbd943534ae4ef102dc9 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 21 Aug 2024 16:48:33 +0300 Subject: [PATCH 265/331] changed entity name to device name when alarm --- .../entitiy/alarm/DefaultTbAlarmService.java | 16 ++++++++-------- .../server/dao/audit/AuditLogServiceImpl.java | 9 +++++---- .../src/assets/locale/locale.constant-en_US.json | 10 +++++----- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 2fb319b829..e54f93be8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -82,7 +82,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb resultAlarm = unassign(alarm, alarm.getAssignTs(), user); } if (result.isModified()) { - logEntityActionService.logEntityAction(tenantId, resultAlarm.getId(), resultAlarm, + logEntityActionService.logEntityAction(tenantId, alarm.getOriginator(), resultAlarm, resultAlarm.getCustomerId(), actionType, user); } return new Alarm(resultAlarm); @@ -107,7 +107,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (result.isModified()) { String systemComment = String.format("Alarm was acknowledged by user %s", user.getTitle()); addSystemAlarmComment(alarmInfo, user, "ACK", systemComment); - logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarmInfo, + logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_ACK, user); } else { throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -130,7 +130,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (result.isCleared()) { String systemComment = String.format("Alarm was cleared by user %s", user.getTitle()); addSystemAlarmComment(alarmInfo, user, "CLEAR", systemComment); - logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarmInfo, + logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_CLEAR, user); } else { throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -149,7 +149,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb AlarmAssignee assignee = alarmInfo.getAssignee(); String systemComment = String.format("Alarm was assigned by user %s to user %s", user.getTitle(), assignee.getTitle()); addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment, assignee.getId()); - logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarmInfo, + logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_ASSIGNED, user); } else { throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -167,7 +167,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (result.isModified()) { String systemComment = String.format("Alarm was unassigned by user %s", user.getTitle()); addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment); - logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarmInfo, + logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_UNASSIGNED, user); } else { throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -195,8 +195,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb @Override public Boolean delete(Alarm alarm, User user) { TenantId tenantId = alarm.getTenantId(); - logEntityActionService.logEntityAction(tenantId, alarm.getId(), alarm, alarm.getCustomerId(), - ActionType.ALARM_DELETE, user, alarm.getOriginator()); + logEntityActionService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(), + ActionType.ALARM_DELETE, user, alarm.getId()); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); } @@ -215,7 +215,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb if (result.isModified()) { String comment = String.format("Alarm was unassigned because user %s - was deleted", userTitle); addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment); - logEntityActionService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getId(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null); + logEntityActionService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null); } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 3f32a1e4ff..c67a105cae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; @@ -123,7 +124,7 @@ public class AuditLogServiceImpl implements AuditLogService { ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; String entityName = "N/A"; - if (entity != null) { + if (entity != null && !(entity instanceof Alarm)) { entityName = entity.getName(); } else { try { @@ -192,9 +193,9 @@ public class AuditLogServiceImpl implements AuditLogService { actionData.set("comment", comment.getComment()); break; case ALARM_DELETE: - actionData.put("entityId", entityId.toString()); - EntityId originatorId = extractParameter(EntityId.class, additionalInfo); - actionData.put("originatorId", originatorId != null ? originatorId.toString() : null); + EntityId alarmId = extractParameter(EntityId.class, additionalInfo); + actionData.put("alarmId", alarmId != null ? alarmId.toString() : null); + actionData.put("originatorId", entityId.toString()); break; case DELETED: case ACTIVATED: 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 9f92072680..f2f8770a21 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -934,11 +934,11 @@ "type-relation-add-or-update": "Relation updated", "type-relation-delete": "Relation deleted", "type-relations-delete": "All relation deleted", - "type-alarm-ack": "Acknowledged", - "type-alarm-clear": "Cleared", - "type-alarm-delete": "Deleted", - "type-alarm-assign": "Assigned", - "type-alarm-unassign": "Unassigned", + "type-alarm-ack": "Alarm acknowledged", + "type-alarm-clear": "Alarm cleared", + "type-alarm-delete": "Alarm deleted", + "type-alarm-assign": "Alarm assigned", + "type-alarm-unassign": "Alarm unassigned", "type-added-comment": "Added comment", "type-updated-comment": "Updated comment", "type-deleted-comment": "Deleted comment", From 0c4507f02a0b83f7b57a492300ad0b7442a37577 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 21 Aug 2024 17:55:34 +0300 Subject: [PATCH 266/331] UI: Add return dialogRef in openDashboardStateInSeparateDialog --- ui-ngx/src/app/core/api/widget-api.models.ts | 3 ++- .../app/modules/home/components/widget/widget.component.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 0300c89d7f..71bfa32ee6 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -63,6 +63,7 @@ import { PopoverPlacement } from '@shared/components/popover.models'; import { PersistentRpc } from '@shared/models/rpc.models'; import { EventEmitter } from '@angular/core'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { MatDialogRef } from '@angular/material/dialog'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -101,7 +102,7 @@ export interface WidgetActionsApi { click: ($event: Event) => void; getActiveEntityInfo: () => SubscriptionEntityInfo; openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, - hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void; + hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => MatDialogRef; openDashboardStateInPopover: ($event: Event, targetDashboardStateId: string, params?: StateParams, hideDashboardToolbar?: boolean, preferredPlacement?: PopoverPlacement, hideOnClickOutside?: boolean, popoverWidth?: string, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 23d5a218e6..76845a11ef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -105,7 +105,7 @@ import { EntityDataService } from '@core/api/entity-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationType } from '@core/notification/notification.models'; import { AlarmDataService } from '@core/api/alarm-data.service'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { ComponentType } from '@angular/cdk/portal'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { MobileService } from '@core/services/mobile.service'; @@ -1365,7 +1365,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, - hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) { + hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number): MatDialogRef { const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); const stateObject: StateObject = {}; stateObject.params = params; @@ -1413,7 +1413,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } }); this.cd.markForCheck(); - return dashboard.dialogRef.afterClosed(); + return dashboard.dialogRef; } private elementClick($event: Event) { From 0599ea0647577fe6cf173745ae5c6d0c623be8c3 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 21 Aug 2024 18:37:53 +0300 Subject: [PATCH 267/331] Added Flow to edit Gateway Config as JSON in Advanced config tab --- .../data/json/tenant/dashboards/gateways.json | 2 +- ...eway-advanced-configuration.component.html | 8 + ...eway-advanced-configuration.component.scss | 23 + ...ateway-advanced-configuration.component.ts | 95 +++ ...ateway-basic-configuration.component.html} | 221 +++--- ...ateway-basic-configuration.component.scss} | 16 +- .../gateway-basic-configuration.component.ts} | 676 +++++++----------- .../gateway-configuration.component.html | 64 ++ .../gateway-configuration.component.scss | 64 ++ .../gateway-configuration.component.ts | 377 ++++++++++ .../models/gateway-configuration.models.ts | 148 ++++ .../gateway/gateway-connectors.component.ts | 16 +- .../lib/gateway/gateway-widget.models.ts | 4 +- .../widget/widget-components.module.ts | 10 +- 14 files changed, 1157 insertions(+), 567 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/{gateway-configuration.component.html => configuration/basic/gateway-basic-configuration.component.html} (77%) rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/{gateway-configuration.component.scss => configuration/basic/gateway-basic-configuration.component.scss} (93%) rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/{gateway-configuration.component.ts => configuration/basic/gateway-basic-configuration.component.ts} (52%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index a62550ed95..8030d3e731 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -200,7 +200,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "customPretty", - "customHtml": "\n

gateway.gateway-configuration

\n \n \n
\n\n\n
\n\n", + "customHtml": "\n\n\n", "customCss": ".gateway-config {\n width: 800px !important;\n padding: 0 !important;\n min-height: 75vh;\n max-width: 100%;\n display: grid !important;\n}\n\n@media screen and (max-width: 599px) {\n .mat-mdc-dialog-content {\n max-height: calc(100% - 60px) !important;\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.device = additionalParams.entity.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n", "customResources": [], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html new file mode 100644 index 0000000000..8ec95a2048 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html @@ -0,0 +1,8 @@ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss new file mode 100644 index 0000000000..4825ff0a64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright ยฉ 2016-2024 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 { + .config-container { + height: calc(100% - 60px); + padding: 8px; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts new file mode 100644 index 0000000000..c806130975 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validators +} from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'tb-gateway-advanced-configuration', + templateUrl: './gateway-advanced-configuration.component.html', + styleUrls: ['./gateway-advanced-configuration.component.scss'], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GatewayAdvancedConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GatewayAdvancedConfigurationComponent), + multi: true + } + ], +}) +export class GatewayAdvancedConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators { + + advancedFormControl: FormControl; + + private onChange: (value: unknown) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.advancedFormControl = this.fb.control(''); + this.advancedFormControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.onChange(value); + this.onTouched(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: unknown) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(basicConfig: unknown): void { + this.advancedFormControl.reset(basicConfig, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.advancedFormControl.valid ? null : { + advancedFormControl: {valid: false} + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html similarity index 77% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html index 36b26cc7a8..b515d1baeb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html @@ -15,10 +15,7 @@ limitations under the License. --> - -

gateway.gateway-configuration

-
- +
@@ -42,23 +39,23 @@ info_outlined - + {{ 'gateway.thingsboard-host-required' | translate }} gateway.thingsboard-port - + {{ 'gateway.thingsboard-port-required' | translate }} - + {{ 'gateway.thingsboard-port-min' | translate }} - + {{ 'gateway.thingsboard-port-max' | translate }} - + {{ 'gateway.thingsboard-port-pattern' | translate }} + *ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')"> security.access-token - + {{ 'security.access-token-required' | translate }} @@ -97,18 +94,18 @@
+ *ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"> security.clientId - + {{ 'security.clientId-required' | translate }} @@ -120,14 +117,14 @@ security.username - + {{ 'security.username-required' | translate }} @@ -138,14 +135,14 @@
+ *ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"> gateway.password @@ -156,13 +153,13 @@
gateway.logs.date-format - + {{ 'gateway.logs.date-format-required' | translate }} gateway.logs.log-format - + {{ 'gateway.logs.log-format-required' | translate }} gateway.logs.file-path - + {{ 'gateway.logs.file-path-required' | translate }}
@@ -244,11 +241,11 @@ gateway.logs.saving-period + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')"> {{ 'gateway.logs.saving-period-required' | translate }} + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')"> {{ 'gateway.logs.saving-period-min' | translate }} @@ -264,11 +261,11 @@ gateway.logs.backup-count + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')"> {{ 'gateway.logs.backup-count-required' | translate }} + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')"> {{ 'gateway.logs.backup-count-min' | translate }} -
{{ 'gateway.hints.' + gatewayConfigGroup.get('storage.type').value | translate }}
- +
{{ 'gateway.hints.' + basicFormGroup.get('storage.type').value | translate }}
+
gateway.storage-read-record-count - + {{ 'gateway.storage-read-record-count-required' | translate }} - + {{ 'gateway.storage-read-record-count-min' | translate }} - + {{ 'gateway.storage-read-record-count-pattern' | translate }} gateway.storage-max-records - + {{ 'gateway.storage-max-records-required' | translate }} - + {{ 'gateway.storage-max-records-min' | translate }} - + {{ 'gateway.storage-max-records-pattern' | translate }} gateway.storage-data-folder-path - + {{ 'gateway.storage-data-folder-path-required' | translate }} gateway.storage-max-files - + {{ 'gateway.storage-max-files-required' | translate }} - + {{ 'gateway.storage-max-files-min' | translate }} - + {{ 'gateway.storage-max-files-pattern' | translate }} gateway.storage-max-read-record-count - + {{ 'gateway.storage-max-read-record-count-required' | translate }} - + {{ 'gateway.storage-max-read-record-count-min' | translate }} - + {{ 'gateway.storage-max-read-record-count-pattern' | translate }} gateway.storage-max-file-records - + {{ 'gateway.storage-max-records-required' | translate }} - + {{ 'gateway.storage-max-records-min' | translate }} - + {{ 'gateway.storage-max-records-pattern' | translate }} gateway.storage-path - + {{ 'gateway.storage-path-required' | translate }} gateway.messages-ttl-check-in-hours - + {{ 'gateway.messages-ttl-check-in-hours-required' | translate }} - + {{ 'gateway.messages-ttl-check-in-hours-min' | translate }} - + {{ 'gateway.messages-ttl-check-in-hours-pattern' | translate }} gateway.messages-ttl-in-days - + {{ 'gateway.messages-ttl-in-days-required' | translate }} - + {{ 'gateway.messages-ttl-in-days-min' | translate }} - + {{ 'gateway.messages-ttl-in-days-pattern' | translate }} info_outlined - + {{ 'gateway.thingsboard-port-required' | translate }} - + {{ 'gateway.thingsboard-port-min' | translate }} - + {{ 'gateway.thingsboard-port-max' | translate }} - + {{ 'gateway.thingsboard-port-pattern' | translate }} @@ -485,13 +482,13 @@ info_outlined - + {{ 'gateway.grpc-keep-alive-timeout-required' | translate }} - + {{ 'gateway.grpc-keep-alive-timeout-min' | translate }} - + {{ 'gateway.grpc-keep-alive-timeout-pattern' | translate }} @@ -503,13 +500,13 @@ info_outlined - + {{ 'gateway.grpc-keep-alive-required' | translate }} - + {{ 'gateway.grpc-keep-alive-min' | translate }} - + {{ 'gateway.grpc-keep-alive-pattern' | translate }} @@ -519,13 +516,13 @@ info_outlined - + {{ 'gateway.grpc-min-time-between-pings-required' | translate }} - + {{ 'gateway.grpc-min-time-between-pings-min' | translate }} - + {{ 'gateway.grpc-min-time-between-pings-pattern' | translate }} @@ -537,13 +534,13 @@ info_outlined - + {{ 'gateway.grpc-max-pings-without-data-required' | translate }} - + {{ 'gateway.grpc-max-pings-without-data-min' | translate }} - + {{ 'gateway.grpc-max-pings-without-data-pattern' | translate }} @@ -553,13 +550,13 @@ info_outlined - + {{ 'gateway.grpc-min-ping-interval-without-data-required' | translate }} - + {{ 'gateway.grpc-min-ping-interval-without-data-min' | translate }} - + {{ 'gateway.grpc-min-ping-interval-without-data-pattern' | translate }} @@ -580,15 +577,15 @@ gateway.statistics.send-period + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')"> {{ 'gateway.statistics.send-period-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')"> {{ 'gateway.statistics.send-period-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')"> {{ 'gateway.statistics.send-period-pattern' | translate }} @@ -643,7 +640,7 @@
@@ -665,7 +662,7 @@
+ [class.no-padding-bottom]="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
@@ -673,20 +670,20 @@
+ *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"> gateway.inactivity-timeout-seconds + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')"> {{ 'gateway.inactivity-timeout-seconds-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')"> {{ 'gateway.inactivity-timeout-seconds-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('pattern')"> {{ 'gateway.inactivity-timeout-seconds-pattern' | translate }} gateway.inactivity-check-period-seconds + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')"> {{ 'gateway.inactivity-check-period-seconds-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')"> {{ 'gateway.inactivity-check-period-seconds-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('pattern')"> {{ 'gateway.inactivity-check-period-seconds-pattern' | translate }} gateway.min-pack-send-delay - + {{ 'gateway.min-pack-send-delay-required' | translate }} - + {{ 'gateway.min-pack-send-delay-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')"> {{ 'gateway.min-pack-send-delay-min-pattern' | translate }} gateway.mqtt-qos - + {{ 'gateway.mqtt-qos-required' | translate }} - + {{ 'gateway.mqtt-qos-range' | translate }} - + {{ 'gateway.mqtt-qos-range' | translate }} gateway.statistics.check-connectors-configuration + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')"> {{ 'gateway.statistics.check-connectors-configuration-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')"> {{ 'gateway.statistics.check-connectors-configuration-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')"> {{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }} @@ -772,15 +769,15 @@ gateway.statistics.max-payload-size-bytes + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')"> {{ 'gateway.statistics.max-payload-size-bytes-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')"> {{ 'gateway.statistics.max-payload-size-bytes-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')"> {{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }} gateway.statistics.min-pack-size-to-send + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('required')"> {{ 'gateway.statistics.min-pack-size-to-send-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('min')"> {{ 'gateway.statistics.min-pack-size-to-send-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')"> {{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }} -
- - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss similarity index 93% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss index 9d72d1d759..9807fb71c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss @@ -23,6 +23,13 @@ display: flex; flex-direction: column; gap: 16px; + max-height: 70vh; + } + + .dialog-mode { + .configuration-block { + max-height: 60vh; + } } .mat-toolbar { @@ -62,15 +69,6 @@ } } - .actions { - grid-row: 3; - padding: 8px; - display: flex; - gap: 8px; - justify-content: flex-end; - flex: 1; - } - mat-form-field { mat-error { display: none !important; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts similarity index 52% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts index 55121da8c5..4b47dd15c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts @@ -14,29 +14,37 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnDestroy, + Output +} from '@angular/core'; +import { + ControlValueAccessor, FormArray, FormBuilder, FormControl, FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { EntityId } from '@shared/models/id/entity-id'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { AttributeService } from '@core/http/attribute.service'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { MatDialog } from '@angular/material/dialog'; import { GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData } from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog'; import { DeviceService } from '@core/http/device.service'; -import { Observable, of } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; import { GatewayLogLevel, GecurityTypesTranslationsMap, @@ -46,51 +54,228 @@ import { LogSavingPeriodTranslations, SecurityTypes, StorageTypes, - StorageTypesTranslationMap -} from './gateway-widget.models'; -import { deepTrim } from '@core/utils'; + StorageTypesTranslationMap, +} from '../../gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + GatewayConfigCommand, + GatewayConfigValue, LogConfig +} from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models'; @Component({ - selector: 'tb-gateway-configuration', - templateUrl: './gateway-configuration.component.html', - styleUrls: ['./gateway-configuration.component.scss'] + selector: 'tb-gateway-basic-configuration', + templateUrl: './gateway-basic-configuration.component.html', + styleUrls: ['./gateway-basic-configuration.component.scss'], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GatewayBasicConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GatewayBasicConfigurationComponent), + multi: true + } + ], }) -export class GatewayConfigurationComponent implements OnInit { +export class GatewayBasicConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators { + + @Input() + device: EntityId; + + @coerceBoolean() + @Input() + dialogMode = false; - gatewayConfigGroup: FormGroup; + @Output() + initialCredentialsUpdated = new EventEmitter(); StorageTypes = StorageTypes; storageTypes = Object.values(StorageTypes) as StorageTypes[]; storageTypesTranslationMap = StorageTypesTranslationMap; - logSavingPeriods = LogSavingPeriodTranslations; - localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[]; localLogsConfigTranslateMap = LocalLogsConfigTranslateMap; - securityTypes = GecurityTypesTranslationsMap; - gatewayLogLevel = Object.values(GatewayLogLevel); - - @Input() - device: EntityId; - - @Input() - dialogRef: MatDialogRef; - logSelector: FormControl; + basicFormGroup: FormGroup; + + private onChange: (value: GatewayConfigValue) => void; + private onTouched: () => void; - private initialCredentials: DeviceCredentials; + private destroy$ = new Subject(); constructor(private fb: FormBuilder, - private attributeService: AttributeService, private deviceService: DeviceService, private cd: ChangeDetectorRef, private dialog: MatDialog) { + this.initBasicFormGroup(); + this.basicFormGroup.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.onChange(value); + this.onTouched(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: GatewayConfigValue) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(basicConfig: GatewayConfigValue): void { + this.basicFormGroup.patchValue(basicConfig, {emitEvent: false}); + if (basicConfig?.thingsboard?.security) { + this.checkAndFetchCredentials(basicConfig.thingsboard.security); + } + if (basicConfig?.grpc) { + this.toggleRpcFields(basicConfig.grpc.enabled); + } + const commands = basicConfig?.thingsboard?.statistics?.commands || []; + commands.forEach(command => this.addCommand(command, false)); + } + + validate(): ValidationErrors | null { + return this.basicFormGroup.valid ? null : { + basicFormGroup: {valid: false} + }; + } + + private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { + return (group: FormGroup): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : {atLeastOne: true}; + }; } - ngOnInit() { - this.gatewayConfigGroup = this.fb.group({ + private toggleRpcFields(enable: boolean): void { + const grpcGroup = this.basicFormGroup.get('grpc') as FormGroup; + if (enable) { + grpcGroup.get('serverPort').enable({emitEvent: false}); + grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false}); + grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false}); + grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false}); + grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false}); + grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false}); + grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false}); + } else { + grpcGroup.get('serverPort').disable({emitEvent: false}); + grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false}); + grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false}); + grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false}); + grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false}); + grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false}); + grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false}); + } + } + + private addLocalLogConfig(name: string, config: LogConfig): void { + const localLogsFormGroup = this.basicFormGroup.get('logs.local') as FormGroup; + const configGroup = this.fb.group({ + logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]], + filePath: [config.filePath || './logs', [Validators.required]], + backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], + savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], + savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] + }); + localLogsFormGroup.addControl(name, configGroup); + } + + getLogFormGroup(value: string): FormGroup { + return this.basicFormGroup.get(`logs.local.${value}`) as FormGroup; + } + + commandFormArray(): FormArray { + return this.basicFormGroup.get('thingsboard.statistics.commands') as FormArray; + } + + removeCommandControl(index: number, event: PointerEvent): void { + if (event.pointerType === '') { + return; + } + this.commandFormArray().removeAt(index); + this.basicFormGroup.markAsDirty(); + } + + private removeAllSecurityValidators(): void { + const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup; + securityGroup.clearValidators(); + for (const controlsKey in securityGroup.controls) { + if (controlsKey !== 'type') { + securityGroup.controls[controlsKey].clearValidators(); + securityGroup.controls[controlsKey].setErrors(null); + securityGroup.controls[controlsKey].updateValueAndValidity(); + } + } + } + + private removeAllStorageValidators(): void { + const storageGroup = this.basicFormGroup.get('storage') as FormGroup; + for (const storageKey in storageGroup.controls) { + if (storageKey !== 'type') { + storageGroup.controls[storageKey].clearValidators(); + storageGroup.controls[storageKey].setErrors(null); + storageGroup.controls[storageKey].updateValueAndValidity(); + } + } + } + + + private openConfigurationConfirmDialog(): void { + this.deviceService.getDevice(this.device.id).pipe(takeUntil(this.destroy$)).subscribe(gateway => { + this.dialog.open + (GatewayRemoteConfigurationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + gatewayName: gateway.name + } + }).afterClosed().subscribe( + (res) => { + if (!res) { + this.basicFormGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); + } + } + ); + }); + } + + addCommand(command?: GatewayConfigCommand, emitEvent: boolean = true): void { + const { attributeOnGateway = null, command: cmd = null, timeout = null } = command || {}; + + const commandFormGroup = this.fb.group({ + attributeOnGateway: [attributeOnGateway, [Validators.required, Validators.pattern(/^[^.\s]+$/)]], + command: [cmd, [Validators.required, Validators.pattern(/^(?=\S).*\S$/)]], + timeout: [timeout, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]] + }); + + this.commandFormArray().push(commandFormGroup, { emitEvent }); + } + + + private initBasicFormGroup(): void { + this.basicFormGroup = this.fb.group({ thingsboard: this.fb.group({ host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]], port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], @@ -125,8 +310,14 @@ export class GatewayConfigurationComponent implements OnInit { }), storage: this.fb.group({ type: [StorageTypes.MEMORY, [Validators.required]], - read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], - max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], + read_records_count: [ + 100, [Validators.min(1), + Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)] + ], + max_records_count: [ + 100000, + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)] + ], data_folder_path: ['./data/', [Validators.required]], max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], @@ -160,18 +351,18 @@ export class GatewayConfigurationComponent implements OnInit { }) }); - this.gatewayConfigGroup.get('thingsboard.security.password').valueChanges.subscribe(password => { + this.basicFormGroup.get('thingsboard.security.password').valueChanges.subscribe(password => { if (password && password !== '') { - this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); + this.basicFormGroup.get('thingsboard.security.username').setValidators([Validators.required]); } else { - this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators(); + this.basicFormGroup.get('thingsboard.security.username').clearValidators(); } - this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); }); this.toggleRpcFields(false); - this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { + this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { if (!enabled) { this.openConfigurationConfirmDialog(); } @@ -180,15 +371,17 @@ export class GatewayConfigurationComponent implements OnInit { this.logSelector = this.fb.control(LocalLogsConfigs.service); for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { - this.addLocalLogConfig(localLogsConfigsKey, {}); + this.addLocalLogConfig(localLogsConfigsKey, {} as LogConfig); } - const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; + const checkingDeviceActivityGroup = this.basicFormGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => { checkingDeviceActivityGroup.updateValueAndValidity(); if (enabled) { - checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); - checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); + checkingDeviceActivityGroup.get('inactivityTimeoutSeconds') + .setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); + checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds') + .setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); } else { checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); @@ -197,11 +390,11 @@ export class GatewayConfigurationComponent implements OnInit { checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false}); }); - this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => { + this.basicFormGroup.get('grpc.enabled').valueChanges.subscribe(value => { this.toggleRpcFields(value); }); - const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; + const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup; securityGroup.get('type').valueChanges.subscribe(type => { this.removeAllSecurityValidators(); if (type === SecurityTypes.ACCESS_TOKEN) { @@ -229,7 +422,7 @@ export class GatewayConfigurationComponent implements OnInit { securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges()); securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges()); - const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; + const storageGroup = this.basicFormGroup.get('storage') as FormGroup; storageGroup.get('type').valueChanges.subscribe(type => { this.removeAllStorageValidators(); if (type === StorageTypes.MEMORY) { @@ -262,403 +455,32 @@ export class GatewayConfigurationComponent implements OnInit { storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false}); } }); - - this.fetchConfigAttribute(this.device); - } - - private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { - return (group: FormGroup): ValidationErrors | null => { - if (!controls) { - controls = Object.keys(group.controls); - } - const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); - - return hasAtLeastOne ? null : {atLeastOne: true}; - }; - } - - private fetchConfigAttribute(entityId: EntityId) { - if (entityId.id === NULL_UUID) { - return; - } - this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, - ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( - mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( - entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration', - 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel'])) - ).subscribe(attributes => { - if (attributes.length) { - const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value; - const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value; - const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value; - const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value; - const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value; - if (general_configuration) { - const configObj = {thingsboard: general_configuration}; - if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) { - for (const command of configObj.thingsboard.statistics.commands) { - this.addCommand(command); - } - delete configObj.thingsboard.statistics.commands; - } - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.gatewayConfigGroup.markAsPristine(); - if (!configObj.thingsboard.remoteConfiguration) { - this.gatewayConfigGroup.disable({emitEvent: false}); - } - this.checkAndFetchCredentials(configObj.thingsboard.security); - } - if (grpc_configuration) { - const configObj = {grpc: grpc_configuration}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.toggleRpcFields(grpc_configuration.enabled); - } - if (logs_configuration) { - const configObj = {logs: this.logsToObj(logs_configuration)}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.cd.detectChanges(); - } - if (storage_configuration) { - const configObj = {storage: storage_configuration}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - } - if (remoteLoggingLevel) { - const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); - remoteLogsFormGroup.patchValue({ - enabled: remoteLoggingLevel !== GatewayLogLevel.NONE, - logLevel: remoteLoggingLevel - }, {emitEvent: false}); - remoteLogsFormGroup.markAsPristine(); - } - this.cd.detectChanges(); - } else { - this.checkAndFetchCredentials(); - } - }); } private checkAndFetchCredentials(security: any = {}): void { if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { - this.initialCredentials = credentials; + this.initialCredentialsUpdated.emit(credentials); if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { - this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN - ? SecurityTypes.TLS_ACCESS_TOKEN - : SecurityTypes.ACCESS_TOKEN); - this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); + this.basicFormGroup.get('thingsboard.security.type') + .setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN + ? SecurityTypes.TLS_ACCESS_TOKEN + : SecurityTypes.ACCESS_TOKEN, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, {emitEvent: false}); if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { - this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); + this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, {emitEvent: false}); } } else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { const parsedValue = JSON.parse(credentials.credentialsValue); - this.gatewayConfigGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD); - this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId); - this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName); - this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.password') + .setValue(parsedValue.password, {emitEvent: false}); } else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE) { //if sertificate is present set sertificate as present } }); } } - - private logsToObj(logsConfig: any) { - const logsObject = { - local: {} - }; - const logFormat = logsConfig.formatters.LogFormatter.format; - const dateFormat = logsConfig.formatters.LogFormatter.datefmt; - for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { - const handlerKey = localLogsConfigsKey + 'Handler'; - logsObject[localLogsConfigsKey] = { - logLevel: logsConfig.loggers[localLogsConfigsKey].level, - filePath: logsConfig.handlers[handlerKey].filename.split('/' + localLogsConfigsKey)[0], - backupCount: logsConfig.handlers[handlerKey].backupCount, - savingTime: logsConfig.handlers[handlerKey].interval, - savingPeriod: logsConfig.handlers[handlerKey].when, - }; - } - - - return {local: logsObject, logFormat, dateFormat}; - } - - private toggleRpcFields(enable: boolean) { - const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup; - if (enable) { - grpcGroup.get('serverPort').enable({emitEvent: false}); - grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false}); - grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false}); - grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false}); - grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false}); - grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false}); - grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false}); - } else { - grpcGroup.get('serverPort').disable({emitEvent: false}); - grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false}); - grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false}); - grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false}); - grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false}); - grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false}); - grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false}); - } - } - - - addCommand(command: any = {}): void { - const commandsFormArray = this.commandFormArray(); - const commandFormGroup = this.fb.group({ - attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]], - command: [command.command || null, [Validators.required, Validators.pattern(/^(?=\S).*\S$/)]], - timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]], - }); - commandsFormArray.push(commandFormGroup); - } - - private addLocalLogConfig(name: string, config: any): void { - const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; - const configGroup = this.fb.group({ - logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]], - filePath: [config.filePath || './logs', [Validators.required]], - backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], - savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], - savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] - }); - localLogsFormGroup.addControl(name, configGroup); - } - - getLogFormGroup(value: string): FormGroup { - return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup; - } - - commandFormArray(): FormArray { - return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray; - } - - removeCommandControl(index: number, event: any): void { - if (event.pointerType === '') { - return; - } - this.commandFormArray().removeAt(index); - this.gatewayConfigGroup.markAsDirty(); - } - - private removeAllSecurityValidators(): void { - const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; - securityGroup.clearValidators(); - for (const controlsKey in securityGroup.controls) { - if (controlsKey !== 'type') { - securityGroup.controls[controlsKey].clearValidators(); - securityGroup.controls[controlsKey].setErrors(null); - securityGroup.controls[controlsKey].updateValueAndValidity(); - } - } - } - - private removeAllStorageValidators(): void { - const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; - for (const storageKey in storageGroup.controls) { - if (storageKey !== 'type') { - storageGroup.controls[storageKey].clearValidators(); - storageGroup.controls[storageKey].setErrors(null); - storageGroup.controls[storageKey].updateValueAndValidity(); - } - } - } - - private removeEmpty(obj: any) { - return Object.fromEntries( - Object.entries(obj) - .filter(([_, v]) => v != null) - .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) - ); - } - - private generateLogsFile(logsObj: any) { - const logAttrObj = { - version: 1, - disable_existing_loggers: false, - formatters: { - LogFormatter: { - class: 'logging.Formatter', - format: logsObj.logFormat, - datefmt: logsObj.dateFormat, - } - }, - handlers: { - consoleHandler: { - class: 'logging.StreamHandler', - formatter: 'LogFormatter', - level: 'DEBUG', - stream: 'ext://sys.stdout' - }, - databaseHandler: { - class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', - formatter: 'LogFormatter', - filename: './logs/database.log', - backupCount: 1, - encoding: 'utf-8' - } - }, - loggers: { - database: { - handlers: ['databaseHandler', 'consoleHandler'], - level: 'DEBUG', - propagate: false - } - }, - root: { - level: 'ERROR', - handlers: [ - 'consoleHandler' - ] - }, - ts: new Date().getTime() - }; - for (const key of Object.keys(logsObj.local)) { - logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(logsObj.local[key], key); - logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key); - } - return logAttrObj; - } - - private createHandlerObj(logObj: any, key: string) { - return { - class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', - formatter: 'LogFormatter', - filename: `${logObj.filePath}/${key}.log`, - backupCount: logObj.backupCount, - interval: logObj.savingTime, - when: logObj.savingPeriod, - encoding: 'utf-8' - }; - } - - private createLoggerObj(logObj: any, key: string) { - return { - handlers: [`${key}Handler`, 'consoleHandler'], - level: logObj.logLevel, - propagate: false - }; - } - - saveConfig(): void { - const value = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); - value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands); - const attributes = []; - attributes.push({ - key: 'RemoteLoggingLevel', - value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE - }); - delete value.connectors; - attributes.push({ - key: 'logs_configuration', - value: this.generateLogsFile(value.logs) - }); - value.grpc.ts = new Date().getTime(); - attributes.push({ - key: 'grpc_configuration', - value: value.grpc - }); - value.storage.ts = new Date().getTime(); - attributes.push({ - key: 'storage_configuration', - value: value.storage - }); - value.thingsboard.ts = new Date().getTime(); - attributes.push({ - key: 'general_configuration', - value: value.thingsboard - }); - - - this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { - this.updateCredentials(value.thingsboard.security).subscribe(() => { - if (this.dialogRef) { - this.dialogRef.close(); - } else { - this.gatewayConfigGroup.markAsPristine(); - this.cd.detectChanges(); - } - }); - }); - } - - private updateCredentials(securityConfig: any): Observable { - let updateCredentials = false; - let newCredentials = {}; - if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { - if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) { - updateCredentials = true; - } else { - const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); - updateCredentials = !( - parsedCredentials.clientId === securityConfig.clientId && - parsedCredentials.userName === securityConfig.username && - parsedCredentials.password === securityConfig.password); - } - if (updateCredentials) { - const credentialsValue: { clientId?: string; userName?: string; password?: string } = {}; - const credentialsType = DeviceCredentialsType.MQTT_BASIC; - if (securityConfig.clientId) { - credentialsValue.clientId = securityConfig.clientId; - } - if (securityConfig.username) { - credentialsValue.userName = securityConfig.username; - } - if (securityConfig.password) { - credentialsValue.password = securityConfig.password; - } - newCredentials = { - credentialsType, - credentialsValue: JSON.stringify(credentialsValue) - }; - } - } else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) { - if (this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN) { - updateCredentials = true; - } else { - updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken; - if (updateCredentials) { - this.initialCredentials.credentialsId = securityConfig.accessToken; - } - } - if (updateCredentials) { - newCredentials = { - credentialsType: DeviceCredentialsType.ACCESS_TOKEN, - credentialsId: securityConfig.accessToken - }; - } - } - - if (updateCredentials) { - return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}); - } - return of(null); - } - - cancel(): void { - if (this.dialogRef) { - this.dialogRef.close(); - } - } - - private openConfigurationConfirmDialog(): void { - this.deviceService.getDevice(this.device.id).subscribe(gateway => { - this.dialog.open - (GatewayRemoteConfigurationDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - gatewayName: gateway.name - } - }).afterClosed().subscribe( - (res) => { - if (!res) { - this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); - } - } - ); - }); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html new file mode 100644 index 0000000000..01fc12f167 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html @@ -0,0 +1,64 @@ + +
+
+ +
+

gateway.gateway-configuration

+
+ + + {{ 'gateway.basic' | translate }} + + + {{ 'gateway.advanced' | translate }} + + + +
+
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss new file mode 100644 index 0000000000..4d1d62ddad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss @@ -0,0 +1,64 @@ +/** + * Copyright ยฉ 2016-2024 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 { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + + .page-header.mat-toolbar { + background: transparent; + color: rgba(0, 0, 0, .87) !important; + } + + .actions { + grid-row: 3; + padding: 8px 16px 8px 8px; + display: flex; + gap: 8px; + justify-content: flex-end; + position: absolute; + bottom: 0; + right: 0; + z-index: 1; + background: white; + width: 100%; + } + + .gateway-config-container { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + + .content-wrapper { + flex: 1; + } + + .toolbar-actions { + display: flex; + align-items: center; + } +} + +.dialog-toggle { + ::ng-deep.mat-button-toggle-button { + color: rgba(255, 255, 255, .75); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts new file mode 100644 index 0000000000..3016d65c72 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts @@ -0,0 +1,377 @@ +import { ChangeDetectorRef, Component, Input, AfterViewInit, OnDestroy } from '@angular/core'; +import { + FormBuilder, + FormGroup, +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { DeviceService } from '@core/http/device.service'; +import { Observable, of, Subject } from 'rxjs'; +import { mergeMap, takeUntil } from 'rxjs/operators'; +import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { + GatewayLogLevel, + SecurityTypes, + ConfigurationModes, + LocalLogsConfigs, + LogSavingPeriod, Attribute +} from '../gateway-widget.models'; +import { deepTrim, isEqual } from '@core/utils'; +import { + GatewayConfigSecurity, + GatewayConfigValue, + GatewayGeneralConfig, + GatewayLogsConfig, + LocalLogs, + LogAttribute, + LogConfig, +} from './models/gateway-configuration.models'; +import { DeviceId } from '@shared/models/id/device-id'; + +@Component({ + selector: 'tb-gateway-configuration', + templateUrl: './gateway-configuration.component.html', + styleUrls: ['./gateway-configuration.component.scss'] +}) +export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy { + + @Input() device: EntityId; + + @Input() dialogRef: MatDialogRef; + + initialCredentials: DeviceCredentials; + gatewayConfigGroup: FormGroup; + ConfigurationModes = ConfigurationModes; + + private destroy$ = new Subject(); + private readonly gatewayConfigAttributeKeys = + ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel', 'mode']; + + constructor(private fb: FormBuilder, + private attributeService: AttributeService, + private deviceService: DeviceService, + private cd: ChangeDetectorRef, + private dialog: MatDialog) { + + this.gatewayConfigGroup = this.fb.group({ + basicConfig: [], + advancedConfig: [], + mode: [ConfigurationModes.BASIC], + }); + + this.observeAlignConfigs(); + } + + ngAfterViewInit(): void { + this.fetchConfigAttribute(this.device); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + saveConfig(): void { + const { mode, advancedConfig } = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); + const value = { mode, ...advancedConfig }; + value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands ?? []); + const attributes = this.generateAttributes(value); + + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { + this.updateCredentials(value.thingsboard.security).subscribe(() => { + if (this.dialogRef) { + this.dialogRef.close(); + } else { + this.gatewayConfigGroup.markAsPristine(); + this.cd.detectChanges(); + } + }); + }); + } + + private observeAlignConfigs(): void { + this.gatewayConfigGroup.get('basicConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { + const advancedControl = this.gatewayConfigGroup.get('advancedConfig'); + + if (!isEqual(advancedControl.value, value)) { + advancedControl.patchValue(value, {emitEvent: false}); + } + }); + + this.gatewayConfigGroup.get('advancedConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { + const basicControl = this.gatewayConfigGroup.get('basicConfig'); + + if (!isEqual(basicControl.value, value)) { + basicControl.patchValue(value, {emitEvent: false}); + } + }); + } + + private generateAttributes(value: GatewayConfigValue): Attribute[] { + const attributes = []; + + const addAttribute = (key: string, val: unknown) => { + attributes.push({ key, value: val }); + }; + + const addTimestampedAttribute = (key: string, val: unknown) => { + val = {...val as Record, ts: new Date().getTime()}; + addAttribute(key, val); + }; + + addAttribute('RemoteLoggingLevel', value.logs?.remote?.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE); + + delete value.connectors; + addAttribute('logs_configuration', this.generateLogsFile(value.logs)); + + addTimestampedAttribute('grpc_configuration', value.grpc); + addTimestampedAttribute('storage_configuration', value.storage); + addTimestampedAttribute('general_configuration', value.thingsboard); + + addAttribute('mode', value.mode); + + return attributes; + } + + private updateCredentials(securityConfig: GatewayConfigSecurity): Observable { + let newCredentials: Partial = {}; + + switch (securityConfig.type) { + case SecurityTypes.USERNAME_PASSWORD: + if (this.shouldUpdateCredentials(securityConfig)) { + newCredentials = this.generateMqttCredentials(securityConfig); + } + break; + + case SecurityTypes.ACCESS_TOKEN: + case SecurityTypes.TLS_ACCESS_TOKEN: + if (this.shouldUpdateAccessToken(securityConfig)) { + newCredentials = { + credentialsType: DeviceCredentialsType.ACCESS_TOKEN, + credentialsId: securityConfig.accessToken + }; + } + break; + } + + return Object.keys(newCredentials).length + ? this.deviceService.saveDeviceCredentials({ ...this.initialCredentials, ...newCredentials }) + : of(null); + } + + private shouldUpdateCredentials(securityConfig: GatewayConfigSecurity): boolean { + if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) { + return true; + } + const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); + return !( + parsedCredentials.clientId === securityConfig.clientId && + parsedCredentials.userName === securityConfig.username && + parsedCredentials.password === securityConfig.password + ); + } + + private generateMqttCredentials(securityConfig: GatewayConfigSecurity): Partial { + const { clientId, username, password } = securityConfig; + + const credentialsValue = { + ...(clientId && { clientId }), + ...(username && { userName: username }), + ...(password && { password }), + }; + + return { + credentialsType: DeviceCredentialsType.MQTT_BASIC, + credentialsValue: JSON.stringify(credentialsValue) + }; + } + + private shouldUpdateAccessToken(securityConfig: GatewayConfigSecurity): boolean { + return this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN || + this.initialCredentials.credentialsId !== securityConfig.accessToken; + } + + cancel(): void { + if (this.dialogRef) { + this.dialogRef.close(); + } + } + + private removeEmpty(obj: unknown) { + return Object.fromEntries( + Object.entries(obj) + .filter(([_, v]) => v != null) + .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) + ); + } + + private generateLogsFile(logsObj: GatewayLogsConfig): LogAttribute { + const logAttrObj = { + version: 1, + disable_existing_loggers: false, + formatters: { + LogFormatter: { + class: 'logging.Formatter', + format: logsObj.logFormat, + datefmt: logsObj.dateFormat, + } + }, + handlers: { + consoleHandler: { + class: 'logging.StreamHandler', + formatter: 'LogFormatter', + level: 'DEBUG', + stream: 'ext://sys.stdout' + }, + databaseHandler: { + class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', + formatter: 'LogFormatter', + filename: './logs/database.log', + backupCount: 1, + encoding: 'utf-8' + } + }, + loggers: { + database: { + handlers: ['databaseHandler', 'consoleHandler'], + level: 'DEBUG', + propagate: false + } + }, + root: { + level: 'ERROR', + handlers: [ + 'consoleHandler' + ] + }, + ts: new Date().getTime() + }; + + this.addLocalLoggers(logAttrObj, logsObj.local); + + return logAttrObj; + } + + private addLocalLoggers(logAttrObj: LogAttribute, localLogs: LocalLogs): void { + for (const key of Object.keys(localLogs)) { + logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(localLogs[key], key); + logAttrObj.loggers[key] = this.createLoggerObj(localLogs[key], key); + } + } + + private createHandlerObj(logObj: LogConfig, key: string) { + return { + class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', + formatter: 'LogFormatter', + filename: `${logObj.filePath}/${key}.log`, + backupCount: logObj.backupCount, + interval: logObj.savingTime, + when: logObj.savingPeriod, + encoding: 'utf-8' + }; + } + + private createLoggerObj(logObj: LogConfig, key: string) { + return { + handlers: [`${key}Handler`, 'consoleHandler'], + level: logObj.logLevel, + propagate: false + }; + } + + private fetchConfigAttribute(entityId: EntityId): void { + if (entityId.id === NULL_UUID) { + return; + } + + this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, + ) + .pipe( + mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( + entityId, AttributeScope.SHARED_SCOPE, this.gatewayConfigAttributeKeys) + ), + takeUntil(this.destroy$) + ) + .subscribe(attributes => { + this.updateConfigs(attributes); + this.cd.detectChanges(); + }); + } + + private updateConfigs(attributes: AttributeData[]): void { + const formValue: GatewayConfigValue = { + thingsboard: null, + grpc: null, + logs: null, + storage: null, + mode: ConfigurationModes.BASIC + }; + + attributes.forEach(attr => { + switch (attr.key) { + case 'general_configuration': + formValue.thingsboard = attr.value; + this.updateFormControls(attr.value); + break; + case 'grpc_configuration': + formValue.grpc = attr.value; + break; + case 'logs_configuration': + formValue.logs = this.logsToObj(attr.value); + break; + case 'storage_configuration': + formValue.storage = attr.value; + break; + case 'mode': + formValue.mode = attr.value; + break; + case 'RemoteLoggingLevel': + formValue.logs = { + ...formValue.logs, + remote: { + enabled: attr.value !== GatewayLogLevel.NONE, + logLevel: attr.value + } + }; + } + }); + + this.gatewayConfigGroup.get('basicConfig').setValue(formValue, { emitEvent: false }); + this.gatewayConfigGroup.get('advancedConfig').setValue(formValue, { emitEvent: false }); + } + + private updateFormControls(thingsboard: GatewayGeneralConfig): void { + const { type, accessToken, ...securityConfig } = thingsboard.security || {}; + + this.initialCredentials = { + deviceId: this.device as DeviceId, + credentialsType: type as unknown as DeviceCredentialsType, + credentialsId: accessToken, + credentialsValue: JSON.stringify(securityConfig) + }; + } + + private logsToObj(logsConfig: LogAttribute): GatewayLogsConfig { + const { format: logFormat, datefmt: dateFormat } = logsConfig.formatters.LogFormatter; + + const localLogs = Object.keys(LocalLogsConfigs).reduce((acc, key) => { + const handler = logsConfig.handlers[`${key}Handler`] || {}; + const logger = logsConfig.loggers[key] || {}; + + acc[key] = { + logLevel: logger.level || GatewayLogLevel.INFO, + filePath: handler.filename?.split(`/${key}`)[0] || './logs', + backupCount: handler.backupCount || 7, + savingTime: handler.interval || 3, + savingPeriod: handler.when || LogSavingPeriod.days + }; + + return acc; + }, {}) as LocalLogs; + + return { local: localLogs, logFormat, dateFormat }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts new file mode 100644 index 0000000000..ba2b7998a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts @@ -0,0 +1,148 @@ +import { + ConfigurationModes, + GatewayConnector, LocalLogsConfigs, LogSavingPeriod, + SecurityTypes, + StorageTypes +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-form.models'; + +export interface GatewayConfigValue { + mode: ConfigurationModes; + thingsboard: GatewayGeneralConfig; + storage: { + type: StorageTypes; + read_records_count?: number; + max_records_count?: number; + data_folder_path?: string; + max_file_count?: number; + max_read_records_count?: number; + max_records_per_file?: number; + data_file_path?: string; + messages_ttl_check_in_hours?: number; + messages_ttl_in_days?: number; + }; + grpc: { + enabled: boolean; + serverPort: number; + keepAliveTimeMs: number; + keepAliveTimeoutMs: number; + keepalivePermitWithoutCalls: boolean; + maxPingsWithoutData: number; + minTimeBetweenPingsMs: number; + minPingIntervalWithoutDataMs: number; + }; + connectors?: GatewayConnector[]; + logs: GatewayLogsConfig; +} + +export interface GatewayGeneralConfig { + host: string; + port: number; + remoteShell: boolean; + remoteConfiguration: boolean; + checkConnectorsConfigurationInSeconds: number; + statistics: { + enable: boolean; + statsSendPeriodInSeconds: number; + commands: GatewayConfigCommand[]; + }; + maxPayloadSizeBytes: number; + minPackSendDelayMS: number; + minPackSizeToSend: number; + handleDeviceRenaming: boolean; + checkingDeviceActivity: { + checkDeviceInactivity: boolean; + inactivityTimeoutSeconds?: number; + inactivityCheckPeriodSeconds?: number; + }; + security: GatewayConfigSecurity; + qos: number; +} + +export interface GatewayLogsConfig { + dateFormat: string; + logFormat: string; + type?: string; + remote?: { + enabled: boolean; + logLevel: GatewayLogLevel; + }; + local: LocalLogs; +} + +export interface GatewayConfigSecurity { + type: SecurityTypes; + accessToken?: string; + clientId?: string; + username?: string; + password?: string; + caCert?: string; + cert?: string; + privateKey?: string; +} + +export interface GatewayConfigCommand { + attributeOnGateway: string; + command: string; + timeout: number; +} + +export interface LogConfig { + logLevel: GatewayLogLevel; + filePath: string; + backupCount: number; + savingTime: number; + savingPeriod: LogSavingPeriod; +} + +export type LocalLogs = Record; + +interface LogFormatterConfig { + class: string; + format: string; + datefmt: string; +} + +interface StreamHandlerConfig { + class: string; + formatter: string; + level: string; + stream: string; +} + +interface FileHandlerConfig { + class: string; + formatter: string; + filename: string; + backupCount: number; + encoding: string; +} + +interface LoggerConfig { + handlers: string[]; + level: string; + propagate: boolean; +} + +interface RootConfig { + level: string; + handlers: string[]; +} + +export interface LogAttribute { + version: number; + disable_existing_loggers: boolean; + formatters: { + LogFormatter: LogFormatterConfig; + }; + handlers: { + consoleHandler: StreamHandlerConfig; + databaseHandler: FileHandlerConfig; + }; + loggers: { + database: LoggerConfig; + }; + root: RootConfig; + ts: number; +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index e7e4d05e45..962d380788 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -52,7 +52,7 @@ import { AddConnectorConfigData, ConnectorBaseConfig, ConnectorBaseInfo, - ConnectorConfigurationModes, + ConfigurationModes, ConnectorType, GatewayAttributeData, GatewayConnector, @@ -97,13 +97,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie readonly gatewayLogLevel = Object.values(GatewayLogLevel); readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; - readonly ConnectorConfigurationModes = ConnectorConfigurationModes; + readonly ConnectorConfigurationModes = ConfigurationModes; pageLink: PageLink; dataSource: MatTableDataSource; connectorForm: FormGroup; activeConnectors: Array; - mode: ConnectorConfigurationModes = this.ConnectorConfigurationModes.BASIC; + mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC; initialConnector: GatewayConnector; private inactiveConnectors: Array; @@ -332,7 +332,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private clearOutConnectorForm(): void { this.initialConnector = null; this.connectorForm.setValue({ - mode: ConnectorConfigurationModes.BASIC, + mode: ConfigurationModes.BASIC, name: '', type: ConnectorType.MQTT, sendDataOnlyOnChange: false, @@ -518,7 +518,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private initConnectorForm(): void { this.connectorForm = this.fb.group({ - mode: [ConnectorConfigurationModes.BASIC], + mode: [ConfigurationModes.BASIC], name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], type: ['', [Validators.required]], enableRemoteLogging: [false], @@ -691,7 +691,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const configJson = this.connectorForm.get('configurationJson'); const type = this.connectorForm.get('type').value; const mode = this.connectorForm.get('mode').value; - if (!isEqual(config, configJson?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.BASIC) { + if (!isEqual(config, configJson?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.BASIC) { const newConfig = {...configJson.value, ...config}; this.connectorForm.get('configurationJson').patchValue(newConfig, {emitEvent: false}); } @@ -708,7 +708,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const basicConfig = this.connectorForm.get('basicConfig'); const type = this.connectorForm.get('type').value; const mode = this.connectorForm.get('mode').value; - if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) { + if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.ADVANCED) { this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); } }); @@ -749,7 +749,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: case ConnectorType.MODBUS: - this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false}); + this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false}); setTimeout(() => { this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.markAsPristine(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 2dde0b8f54..4b0f684bc7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -123,7 +123,7 @@ export interface GatewayConnector { logLevel: string; key?: string; class?: string; - mode?: ConnectorConfigurationModes; + mode?: ConfigurationModes; } export interface DataMapping { @@ -493,7 +493,7 @@ export interface ModbusSlaveInfo { buttonTitle: string; } -export enum ConnectorConfigurationModes { +export enum ConfigurationModes { BASIC = 'basic', ADVANCED = 'advanced' } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 955fd1ecf6..2acc299586 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -56,7 +56,7 @@ import { GatewayServiceRPCConnectorTemplatesComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component'; import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component'; -import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component'; +import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/configuration/gateway-configuration.component'; import { GatewayRemoteConfigurationDialogComponent } from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog'; @@ -145,6 +145,12 @@ import { ModbusRpcParametersComponent } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; +import { + GatewayBasicConfigurationComponent +} from '@home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component'; +import { + GatewayAdvancedConfigurationComponent +} from '@home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component'; @NgModule({ declarations: [ @@ -233,6 +239,8 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc ModbusBasicConfigComponent, EllipsisChipListDirective, ModbusRpcParametersComponent, + GatewayBasicConfigurationComponent, + GatewayAdvancedConfigurationComponent, ], exports: [ EntitiesTableWidgetComponent, From f0123d7c7b70c7c6808acc886d3dc25f41f7b937 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 21 Aug 2024 18:46:49 +0300 Subject: [PATCH 268/331] license headers --- ...ateway-advanced-configuration.component.html | 17 +++++++++++++++++ .../gateway-configuration.component.ts | 16 ++++++++++++++++ .../models/gateway-configuration.models.ts | 16 ++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html index 8ec95a2048..843e67b0fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html @@ -1,3 +1,20 @@ + Date: Thu, 22 Aug 2024 11:30:40 +0300 Subject: [PATCH 269/331] added tests --- .../server/common/data/audit/ActionType.java | 92 +++++++----- .../common/data/audit/ActionTypeTest.java | 23 ++- .../server/dao/audit/AuditLogServiceImpl.java | 18 ++- .../dao/audit/AuditLogServiceImplTest.java | 141 ++++++++++++++++++ 4 files changed, 228 insertions(+), 46 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index db56acb8b2..3a70dacf65 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -22,52 +22,68 @@ import java.util.Optional; public enum ActionType { - ADDED(false, TbMsgType.ENTITY_CREATED), // log entity - DELETED(false, TbMsgType.ENTITY_DELETED), // log string id - UPDATED(false, TbMsgType.ENTITY_UPDATED), // log entity - ATTRIBUTES_UPDATED(false, TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values - ATTRIBUTES_DELETED(false, TbMsgType.ATTRIBUTES_DELETED), // log attributes - TIMESERIES_UPDATED(false, TbMsgType.TIMESERIES_UPDATED), // log timeseries update - TIMESERIES_DELETED(false, TbMsgType.TIMESERIES_DELETED), // log timeseries - RPC_CALL(false, null), // log method and params - CREDENTIALS_UPDATED(false, null), // log new credentials - ASSIGNED_TO_CUSTOMER(false, TbMsgType.ENTITY_ASSIGNED), // log customer name - UNASSIGNED_FROM_CUSTOMER(false, TbMsgType.ENTITY_UNASSIGNED), // log customer name - ACTIVATED(false, null), // log string id - SUSPENDED(false, null), // log string id - CREDENTIALS_READ(true, null), // log device id - ATTRIBUTES_READ(true, null), // log attributes - RELATION_ADD_OR_UPDATE(false, TbMsgType.RELATION_ADD_OR_UPDATE), - RELATION_DELETED(false, TbMsgType.RELATION_DELETED), - RELATIONS_DELETED(false, TbMsgType.RELATIONS_DELETED), - REST_API_RULE_ENGINE_CALL(false, null), // log call to rule engine from REST API - ALARM_ACK(false, TbMsgType.ALARM_ACK), - ALARM_CLEAR(false, TbMsgType.ALARM_CLEAR), - ALARM_DELETE(false, TbMsgType.ALARM_DELETE), - ALARM_ASSIGNED(false, TbMsgType.ALARM_ASSIGNED), - ALARM_UNASSIGNED(false, TbMsgType.ALARM_UNASSIGNED), - LOGIN(false, null), - LOGOUT(false, null), - LOCKOUT(false, null), - ASSIGNED_FROM_TENANT(false, TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), - ASSIGNED_TO_TENANT(false, TbMsgType.ENTITY_ASSIGNED_TO_TENANT), - PROVISION_SUCCESS(false, TbMsgType.PROVISION_SUCCESS), - PROVISION_FAILURE(false, TbMsgType.PROVISION_FAILURE), - ASSIGNED_TO_EDGE(false, TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name - UNASSIGNED_FROM_EDGE(false, TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), - ADDED_COMMENT(false, TbMsgType.COMMENT_CREATED), - UPDATED_COMMENT(false, TbMsgType.COMMENT_UPDATED), - DELETED_COMMENT(false, null), - SMS_SENT(false, null); + ADDED(TbMsgType.ENTITY_CREATED), // log entity + DELETED(TbMsgType.ENTITY_DELETED), // log string id + UPDATED(TbMsgType.ENTITY_UPDATED), // log entity + ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values + ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes + TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update + TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries + RPC_CALL(null), // log method and params + CREDENTIALS_UPDATED(null), // log new credentials + ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name + UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name + ACTIVATED(null), // log string id + SUSPENDED(null), // log string id + CREDENTIALS_READ(true), // log device id + ATTRIBUTES_READ(true), // log attributes + RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), + RELATION_DELETED(TbMsgType.RELATION_DELETED), + RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), + REST_API_RULE_ENGINE_CALL(null), // log call to rule engine from REST API + ALARM_ACK(TbMsgType.ALARM_ACK, true), + ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), + ALARM_DELETE(TbMsgType.ALARM_DELETE, true), + ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), + ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), + LOGIN(null), + LOGOUT(null), + LOCKOUT(null), + ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), + ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), + PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), + PROVISION_FAILURE(TbMsgType.PROVISION_FAILURE), + ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name + UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), + ADDED_COMMENT(TbMsgType.COMMENT_CREATED), + UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), + DELETED_COMMENT(null), + SMS_SENT(null); @Getter private final boolean isRead; private final TbMsgType ruleEngineMsgType; - ActionType(boolean isRead, TbMsgType ruleEngineMsgType) { + @Getter + private final boolean alarmAction; + + ActionType(boolean isRead) { this.isRead = isRead; + this.ruleEngineMsgType = null; + this.alarmAction = false; + } + + ActionType(TbMsgType ruleEngineMsgType) { + this.isRead = false; + this.ruleEngineMsgType = ruleEngineMsgType; + this.alarmAction = false; + } + + ActionType(TbMsgType ruleEngineMsgType, boolean isAlarmAction) { + this.isRead = false; this.ruleEngineMsgType = ruleEngineMsgType; + this.alarmAction = isAlarmAction; } public Optional getRuleEngineMsgType() { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java index 6a5ed54896..ddfe068937 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java @@ -21,6 +21,11 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.audit.ActionType.ACTIVATED; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_ACK; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_ASSIGNED; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_DELETE; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_UNASSIGNED; import static org.thingsboard.server.common.data.audit.ActionType.ATTRIBUTES_READ; import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_READ; import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_UPDATED; @@ -35,7 +40,7 @@ import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED; class ActionTypeTest { - private static final List typesWithNullRuleEngineMsgType = List.of( + private final List typesWithNullRuleEngineMsgType = List.of( RPC_CALL, CREDENTIALS_UPDATED, ACTIVATED, @@ -50,6 +55,10 @@ class ActionTypeTest { REST_API_RULE_ENGINE_CALL ); + private final List alarmActionTypes = List.of( + ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, ALARM_ASSIGNED, ALARM_UNASSIGNED + ); + // backward-compatibility tests @Test @@ -64,4 +73,16 @@ class ActionTypeTest { } } + @Test + void isAlarmActionTest() { + var types = ActionType.values(); + for (var type : types) { + if (alarmActionTypes.contains(type)) { + assertThat(type.isAlarmAction()).isTrue(); + } else { + assertThat(type.isAlarmAction()).isFalse(); + } + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index c67a105cae..933909e7e9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -29,8 +29,8 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -124,13 +124,17 @@ public class AuditLogServiceImpl implements AuditLogService { ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; String entityName = "N/A"; - if (entity != null && !(entity instanceof Alarm)) { - entityName = entity.getName(); - } else { - try { - entityName = entityService.fetchEntityName(tenantId, entityId).orElse(entityName); - } catch (Exception ignored) { + if (entity != null) { + if (actionType.isAlarmAction()) { + entityName = (entity instanceof AlarmInfo alarmInfo) + ? alarmInfo.getOriginatorName() + : entityService.fetchEntityName(tenantId, entityId).orElse(entityName); + } else { + entityName = entity.getName(); } + } else try { + entityName = entityService.fetchEntityName(tenantId, entityId).orElse(entityName); + } catch (Exception ignored) { } if (e != null) { actionStatus = ActionStatus.FAILURE; diff --git a/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java new file mode 100644 index 0000000000..2a876c58db --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java @@ -0,0 +1,141 @@ +/** + * Copyright ยฉ 2016-2024 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.dao.audit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.audit.sink.AuditLogSink; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.service.validator.AuditLogDataValidator; +import org.thingsboard.server.dao.sql.JpaExecutorService; + +import java.util.UUID; +import java.util.concurrent.Callable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; + +@ExtendWith(MockitoExtension.class) +public class AuditLogServiceImplTest { + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("9114e9ac-6c28-4019-a2a7-b948cb9500d5")); + private final CustomerId CUSTOMER_ID = new CustomerId(UUID.fromString("d15822ef-09eb-49a6-9068-21b9c8ae3356")); + private final UserId USER_ID = new UserId(UUID.fromString("47a2c904-3a47-4530-91bb-51068a4610a7")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b913c12a-9942-4cbd-9481-c42dad4831b0")); + + private final String USER_NAME = "Test User"; + + @InjectMocks + private AuditLogServiceImpl auditLogService; + @Mock + private EntityService entityService; + @Mock + private AuditLogLevelFilter auditLogLevelFilter; + @Mock + private AuditLogDataValidator auditLogDataValidator; + @Mock + private JpaExecutorService executor; + @Mock + private AuditLogDao auditLogDao; + @Mock + private AuditLogSink auditLogSink; + + @Test + public void givenEntityIsNull_whenLogEntityAction_thenShouldFetchEntityName() { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, null, ActionType.ADDED, null); + + // THEN + then(entityService).should().fetchEntityName(TENANT_ID, DEVICE_ID); + } + + @Test + public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldGetEntityName() throws Exception { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + Alarm alarm = new Alarm(new AlarmId(UUID.fromString("55f577b3-6ef5-4b99-92dc-70eb78b2a970"))); + alarm.setType("Test Alarm"); + AlarmComment comment = new AlarmComment(); + comment.setComment(JacksonUtil.toJsonNode("{\"comment\": \"test\"}")); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, alarm.getId(), alarm, ActionType.ADDED_COMMENT, null, comment); + + // THEN + then(entityService).should(never()).fetchEntityName(any(), any()); + ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); + then(executor).should().submit(submitTask.capture()); + submitTask.getValue().call(); + ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); + then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); + assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo("Test Alarm"); + } + + @Test + public void givenActionTypeIsAlarmActionAndEntityIsAlarmInfo_whenLogEntityAction_thenShouldGetEntityOriginatorName() throws Exception { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + AlarmInfo alarmInfo = new AlarmInfo(); + alarmInfo.setOriginatorName("Test Device"); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, alarmInfo, ActionType.ALARM_ASSIGNED, null); + + // THEN + then(entityService).should(never()).fetchEntityName(any(), any()); + ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); + then(executor).should().submit(submitTask.capture()); + submitTask.getValue().call(); + ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); + then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); + assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo("Test Device"); + } + + @Test + public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldFetchEntityName() { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, new Alarm(), ActionType.ALARM_DELETE, null); + + // THEN + then(entityService).should().fetchEntityName(TENANT_ID, DEVICE_ID); + } + +} From f0186ca996993cf282571ab86f907067bc03abf9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 22 Aug 2024 12:31:52 +0300 Subject: [PATCH 270/331] Fix entity update query executed twice (resulting in double version increment) --- .../dao/model/sql/DeviceProfileEntity.java | 2 ++ .../server/dao/sql/JpaAbstractDao.java | 25 ++++++++++++++----- .../dao/sql/JpaPartitionedAbstractDao.java | 4 +-- .../dao/sql/asset/JpaAssetProfileDao.java | 9 ------- .../dao/sql/device/JpaDeviceProfileDao.java | 9 ------- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index c5e6bc4968..7b3bbe2db2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -25,6 +25,7 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.ToString; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLJsonPGObjectJsonbType; import org.thingsboard.common.util.JacksonUtil; @@ -48,6 +49,7 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.DEVICE_PROFILE_TABLE_NAME) +@ToString(callSuper = true) public final class DeviceProfileEntity extends BaseVersionedEntity { @Column(name = ModelConstants.DEVICE_PROFILE_TENANT_ID_PROPERTY) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 8002730686..801898386e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -58,6 +58,10 @@ public abstract class JpaAbstractDao, D> @Override @Transactional public D save(TenantId tenantId, D domain) { + return save(tenantId, domain, false); + } + + private D save(TenantId tenantId, D domain, boolean flush) { E entity; try { entity = getEntityClass().getConstructor(domain.getClass()).newInstance(domain); @@ -73,14 +77,15 @@ public abstract class JpaAbstractDao, D> entity.setCreatedTime(Uuids.unixTimestamp(uuid)); } try { - entity = doSave(entity, isNew); + entity = doSave(entity, isNew, flush); } catch (OptimisticLockException e) { throw new EntityVersionMismatchException((getEntityType() != null ? getEntityType().getNormalName() : "Entity") + " was already changed by someone else", e); } return DaoUtil.getData(entity); } - protected E doSave(E entity, boolean isNew) { + protected E doSave(E entity, boolean isNew, boolean flush) { + boolean flushed = false; EntityManager entityManager = getEntityManager(); if (isNew) { if (entity instanceof HasVersion versionedEntity) { @@ -94,24 +99,32 @@ public abstract class JpaAbstractDao, D> if (existingEntity != null) { versionedEntity.setVersion(existingEntity.getVersion()); // manually resetting the version to latest to allow force overwrite of the entity } else { - return doSave(entity, true); + return doSave(entity, true, flush); } } entity = entityManager.merge(entity); + /* + * flushing so that the query is executed right away and the version is incremented, + * then removing the entity from the persistence context so that it is not affected + * by next flushes (e.g. when a transaction is committed) to avoid double version increment + * */ entityManager.flush(); + entityManager.detach(entity); + flushed = true; } else { entity = entityManager.merge(entity); } } + if (flush && !flushed) { + entityManager.flush(); + } return entity; } @Override @Transactional public D saveAndFlush(TenantId tenantId, D domain) { - D d = save(tenantId, domain); - getRepository().flush(); - return d; + return save(tenantId, domain, true); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java index 6afb8b4ede..a74f32d8c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java @@ -22,9 +22,9 @@ import org.thingsboard.server.dao.util.SqlDao; public abstract class JpaPartitionedAbstractDao, D> extends JpaAbstractDao { @Override - protected E doSave(E entity, boolean isNew) { + protected E doSave(E entity, boolean isNew, boolean flush) { createPartition(entity); - return super.doSave(entity, isNew); + return super.doSave(entity, isNew, flush); } public abstract void createPartition(E entity); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index d6025fc07b..6ed74a5aa9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -19,7 +19,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -58,14 +57,6 @@ public class JpaAssetProfileDao extends JpaAbstractDao findAssetProfiles(TenantId tenantId, PageLink pageLink) { return DaoUtil.toPageData( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 0f597756eb..4e1594eeff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -19,7 +19,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; @@ -62,14 +61,6 @@ public class JpaDeviceProfileDao extends JpaAbstractDao findDeviceProfiles(TenantId tenantId, PageLink pageLink) { return DaoUtil.toPageData( From d5ee96e29542d5a98fc938560dba254503430da6 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 22 Aug 2024 12:36:05 +0300 Subject: [PATCH 271/331] UI: SCADA Heat pump --- .../system/scada_symbols/left-heat-pump.svg | 1035 ++++++++++++++++ .../system/scada_symbols/right-heat-pump.svg | 1044 +++++++++++++++++ .../scada_water_system_symbols.json | 2 + .../assets/locale/locale.constant-en_US.json | 15 +- 4 files changed, 2095 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/json/system/scada_symbols/left-heat-pump.svg create mode 100644 application/src/main/data/json/system/scada_symbols/right-heat-pump.svg diff --git a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg new file mode 100644 index 0000000000..1c9fdff63d --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg @@ -0,0 +1,1035 @@ + +{ + "title": "Left heat pump", + "description": "Left heat pump with configurable connectors, running animation and various states.", + "searchTags": [ + "pump", + "heat" + ], + "widgetSizeX": 4, + "widgetSizeY": 3, + "stateRenderFunction": "var levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\nvar levelArrowUp = ctx.tags.levelArrowUp;\nvar levelArrowDown = ctx.tags.levelArrowDown;\nvar powerButtonBackground = ctx.tags.powerButtonBackground;\nvar powerButtonIcons = ctx.tags.powerButtonIcon;\n\nvar temperature = ctx.values.temperature;\nvar running = ctx.values.running;\nvar minTemperature = ctx.properties.minTemperature;\nvar maxTemperature = ctx.properties.maxTemperature;\nvar disabledPowerColor = ctx.properties.disabledPowerButtonBackground;\nvar enabledPowerColor = ctx.properties.powerButtonBackground;\n\nvar levelUpEnabled = running && temperature < maxTemperature;\nvar levelDownEnabled = running && temperature > minTemperature;\n\nif (running) {\n powerButtonBackground[0].attr({fill: enabledPowerColor});\n powerButtonIcons.forEach(e => e.attr({fill: disabledPowerColor}));\n} else {\n powerButtonBackground[0].attr({fill: disabledPowerColor});\n powerButtonIcons.forEach(e => e.attr({fill: enabledPowerColor}));\n}\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelArrowUp[0].attr({fill: '#647484'});\n} else {\n ctx.api.disable(levelUpButton);\n levelArrowUp[0].attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelArrowDown[0].attr({fill: '#647484'});\n} else {\n ctx.api.disable(levelDownButton);\n levelArrowDown[0].attr({fill: '#777'});\n}", + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.heatPumpColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fan", + "stateRenderFunction": "var running = ctx.values.running;\nvar t = element.timeline();\nif (running) {\n if (!t.active()) {\n if (t.time()) {\n t.play();\n } else {\n ctx.api.animate(element, 2000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n }\n var speed = ctx.values.rotationAnimationSpeed;\n t.speed(speed);\n} else {\n t.pause();\n}\n", + "actions": null + }, + { + "tag": "fan-blade", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "levelDownButton", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var minTemperature = ctx.properties.minTemperature;\n var step = ctx.properties.temperatureStep;\n \n var newTemperature = Math.max(minTemperature, temperature - step);\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" + } + } + }, + { + "tag": "levelUpButton", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var maxTemperature = ctx.properties.maxTemperature;\n var step = ctx.properties.temperatureStep;\n \n var newTemperature = Math.min(maxTemperature, temperature + step);\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" + } + } + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "power-button", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "var rinning = ctx.values.running;\nvar action = rinning ? 'stop' : 'run';\n\nif (!ctx.values.temperature) {\n ctx.api.setValue('temperature', ctx.properties.minTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', ctx.properties.minTemperature, {\n error: () => {\n ctx.api.setValue('temperature', ctx.values.temperature);\n }\n });\n}\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('running', !rinning);\n }\n});" + } + } + }, + { + "tag": "value-box-background", + "stateRenderFunction": "var color = ctx.properties.valueBoxBackground;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", + "actions": null + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "temperature", + "name": "{i18n:scada.symbol.temperature}", + "hint": "{i18n:scada.symbol.temperature-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "temperature" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "updateTemperatureState", + "name": "{i18n:scada.symbol.update-temperature}", + "hint": "{i18n:scada.symbol.update-temperature-hint}", + "group": null, + "type": "action", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "ADD_TIME_SERIES", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SERVER_SCOPE", + "key": "state" + }, + "putTimeSeries": { + "key": "temperature" + }, + "valueToData": { + "type": "VALUE", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "run", + "name": "{i18n:scada.symbol.run}", + "hint": "{i18n:scada.symbol.run-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "stop", + "name": "{i18n:scada.symbol.stop}", + "hint": "{i18n:scada.symbol.stop-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rotationAnimationSpeed", + "name": "{i18n:scada.symbol.rotation-animation-speed}", + "hint": "{i18n:scada.symbol.rotation-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "minTemperature", + "name": "{i18n:scada.symbol.temperature}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "Min", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "column-xs", + "fieldClass": "", + "min": null, + "max": null, + "step": 1 + }, + { + "id": "maxTemperature", + "name": "{i18n:scada.symbol.temperature}", + "type": "number", + "default": 45, + "required": true, + "subLabel": "Max", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": 1 + }, + { + "id": "temperatureStep", + "name": "{i18n:scada.symbol.temperature-step}", + "type": "number", + "default": 1, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": 0.5 + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#000000C2", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 40, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-units}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "units", + "name": "{i18n:scada.symbol.value-units}", + "type": "units", + "default": "ยฐC", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxBackground", + "name": "{i18n:scada.symbol.value-box-background}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "powerButtonBackground", + "name": "{i18n:scada.symbol.power-button-background}", + "type": "color", + "default": "#1ABB48", + "required": null, + "subLabel": "Enabled", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "column-xs", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "disabledPowerButtonBackground", + "name": "{i18n:scada.symbol.power-button-background}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": "Disabled", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "heatPumpColor", + "name": "{i18n:scada.symbol.heat-pump-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg new file mode 100644 index 0000000000..c6253b9a7a --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg @@ -0,0 +1,1044 @@ + +{ + "title": "Right heat pump", + "description": "Right heat pump with configurable connectors, running animation and various states.", + "searchTags": [ + "pump", + "heat" + ], + "widgetSizeX": 4, + "widgetSizeY": 3, + "stateRenderFunction": "var levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\nvar levelArrowUp = ctx.tags.levelArrowUp;\nvar levelArrowDown = ctx.tags.levelArrowDown;\nvar powerButtonBackground = ctx.tags.powerButtonBackground;\nvar powerButtonIcons = ctx.tags.powerButtonIcon;\n\nvar temperature = ctx.values.temperature;\nvar running = ctx.values.running;\nvar minTemperature = ctx.properties.minTemperature;\nvar maxTemperature = ctx.properties.maxTemperature;\nvar disabledPowerColor = ctx.properties.disabledPowerButtonBackground;\nvar enabledPowerColor = ctx.properties.powerButtonBackground;\n\nvar levelUpEnabled = running && temperature < maxTemperature;\nvar levelDownEnabled = running && temperature > minTemperature;\n\nif (running) {\n powerButtonBackground[0].attr({fill: enabledPowerColor});\n powerButtonIcons.forEach(e => e.attr({fill: disabledPowerColor}));\n} else {\n powerButtonBackground[0].attr({fill: disabledPowerColor});\n powerButtonIcons.forEach(e => e.attr({fill: enabledPowerColor}));\n}\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelArrowUp[0].attr({fill: '#647484'});\n} else {\n ctx.api.disable(levelUpButton);\n levelArrowUp[0].attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelArrowDown[0].attr({fill: '#647484'});\n} else {\n ctx.api.disable(levelDownButton);\n levelArrowDown[0].attr({fill: '#777'});\n}", + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.heatPumpColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fan", + "stateRenderFunction": "var running = ctx.values.running;\nvar t = element.timeline();\nif (running) {\n if (!t.active()) {\n if (t.time()) {\n t.play();\n } else {\n ctx.api.animate(element, 2000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n }\n var speed = ctx.values.rotationAnimationSpeed;\n t.speed(speed);\n} else {\n t.pause();\n}\n", + "actions": null + }, + { + "tag": "fan-blade", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "levelDownButton", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var minTemperature = ctx.properties.minTemperature;\n var step = ctx.properties.temperatureStep;\n \n var newTemperature = Math.max(minTemperature, temperature - step);\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" + } + } + }, + { + "tag": "levelUpButton", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var maxTemperature = ctx.properties.maxTemperature;\n var step = ctx.properties.temperatureStep;\n \n var newTemperature = Math.min(maxTemperature, temperature + step);\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" + } + } + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "power-button", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "var rinning = ctx.values.running;\nvar action = rinning ? 'stop' : 'run';\n\nif (!ctx.values.temperature) {\n ctx.api.setValue('temperature', ctx.properties.minTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', ctx.properties.minTemperature, {\n error: () => {\n ctx.api.setValue('temperature', ctx.values.temperature);\n }\n });\n}\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('running', !rinning);\n }\n});" + } + } + }, + { + "tag": "value-box-background", + "stateRenderFunction": "var color = ctx.properties.valueBoxBackground;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", + "actions": null + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "temperature", + "name": "{i18n:scada.symbol.temperature}", + "hint": "{i18n:scada.symbol.temperature-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "temperature" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "updateTemperatureState", + "name": "{i18n:scada.symbol.update-temperature}", + "hint": "{i18n:scada.symbol.update-temperature-hint}", + "group": null, + "type": "action", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "ADD_TIME_SERIES", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SERVER_SCOPE", + "key": "state" + }, + "putTimeSeries": { + "key": "temperature" + }, + "valueToData": { + "type": "VALUE", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "run", + "name": "{i18n:scada.symbol.run}", + "hint": "{i18n:scada.symbol.run-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "stop", + "name": "{i18n:scada.symbol.stop}", + "hint": "{i18n:scada.symbol.stop-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rotationAnimationSpeed", + "name": "{i18n:scada.symbol.rotation-animation-speed}", + "hint": "{i18n:scada.symbol.rotation-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "minTemperature", + "name": "{i18n:scada.symbol.temperature}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "Min", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "column-xs", + "fieldClass": "", + "min": null, + "max": null, + "step": 1 + }, + { + "id": "maxTemperature", + "name": "{i18n:scada.symbol.temperature}", + "type": "number", + "default": 45, + "required": true, + "subLabel": "Max", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": 1 + }, + { + "id": "temperatureStep", + "name": "{i18n:scada.symbol.temperature-step}", + "type": "number", + "default": 1, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": 0.5 + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#000000C2", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 40, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-units}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "units", + "name": "{i18n:scada.symbol.value-units}", + "type": "units", + "default": "ยฐC", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxBackground", + "name": "{i18n:scada.symbol.value-box-background}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "powerButtonBackground", + "name": "{i18n:scada.symbol.power-button-background}", + "type": "color", + "default": "#1ABB48", + "required": null, + "subLabel": "Enabled", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "column-xs", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "disabledPowerButtonBackground", + "name": "{i18n:scada.symbol.power-button-background}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": "Disabled", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "heatPumpColor", + "name": "{i18n:scada.symbol.heat-pump-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + 27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json index 224938c019..1d647d0468 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -39,6 +39,8 @@ "small_left_motor_pump", "right_motor_pump", "left_motor_pump", + "right_heat_pump", + "left_heat_pump", "short_bottom_filter", "long_bottom_filter", "short_top_filter", 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 b7e96b9faf..a447f3290b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3815,7 +3815,20 @@ "major-ticks-color": "Major ticks color", "normal": "Normal", "minor-ticks": "Minor ticks", - "minor-ticks-color": "Minor ticks color" + "minor-ticks-color": "Minor ticks color", + "temperature": "Temperature", + "temperature-hint": "Double value indicating the current temperature.", + "update-temperature": "Update temperature", + "update-temperature-hint": "Action invoked when user clicks to change current temperature.", + "run": "Run", + "run-hint": "Action invoked when user clicks to run component.", + "stop": "Stop", + "stop-hint": "Action invoked when user clicks to stop component.", + "temperature-step": "Temperature step increment", + "heat-pump-color": "Heat pump color", + "power-button-background": "Power button background", + "value-box-background": "Value box background", + "value-units": "Value units" } }, "item": { From 98ac14dfbedbef3d1c57591394066047fb4e49f4 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 22 Aug 2024 13:08:45 +0300 Subject: [PATCH 272/331] VC: Fix error message when using https url with private key authentication --- .../org/thingsboard/server/service/sync/vc/GitRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java index 751945a360..aaa4c22289 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java @@ -467,6 +467,9 @@ public class GitRepository { if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); } else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { + if (StringUtils.startsWith(settings.getRepositoryUri(), "https://")) { + throw new IllegalArgumentException("Invalid URI format for private key authentication"); + } sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); } return new AuthHandler(credentialsProvider, sshSessionFactory); From 69b89ebb793c4d07135b20c1ef0b6d8c4504c712 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 22 Aug 2024 13:45:33 +0300 Subject: [PATCH 273/331] Ignore unknown notification recipients --- .../controller/NotificationController.java | 10 ++++++++-- .../DefaultNotificationCenter.java | 20 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index fb32ebd30b..c1248a1fbf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -300,9 +300,15 @@ public class NotificationController extends BaseController { request.setOriginatorEntityId(user.getId()); List targets = request.getTargets().stream() .map(NotificationTargetId::new) - .map(targetId -> notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId)) + .map(targetId -> { + NotificationTarget target = notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId); + if (target == null) { + throw new IllegalArgumentException("Notification target for id " + targetId + " not found"); + } + return target; + }) .sorted(Comparator.comparing(target -> target.getConfiguration().getType())) - .collect(Collectors.toList()); + .toList(); NotificationRequestPreview preview = new NotificationRequestPreview(); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index f20b666b28..84d3b6c378 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -73,6 +73,7 @@ import org.thingsboard.server.service.telemetry.AbstractSubscriptionService; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -115,12 +116,23 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } else { notificationTemplate = request.getTemplate(); } - if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing"); + if (notificationTemplate == null) { + throw new IllegalArgumentException("Template is missing"); + } Set deliveryMethods = new HashSet<>(); - List targets = request.getTargets().stream().map(NotificationTargetId::new) - .map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)) - .collect(Collectors.toList()); + List targets = new ArrayList<>(); + for (UUID targetId : request.getTargets()) { + NotificationTarget target = notificationTargetService.findNotificationTargetById(tenantId, new NotificationTargetId(targetId)); + if (target != null) { + targets.add(target); + } else { + log.debug("Unknown notification target {} in request {}", targetId, request); + } + } + if (targets.isEmpty()) { + throw new IllegalArgumentException("No recipients chosen"); + } NotificationRuleId ruleId = request.getRuleId(); notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { From 5e033ff066c906031a812e4332bf875422285fcd Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 22 Aug 2024 14:04:33 +0300 Subject: [PATCH 274/331] added additional verification in tests --- .../server/common/data/audit/ActionType.java | 44 ++++++++++--------- .../common/data/audit/ActionTypeTest.java | 21 +++++++-- .../server/dao/audit/AuditLogServiceImpl.java | 35 +++++++++------ .../dao/audit/AuditLogServiceImplTest.java | 41 +++++++++-------- 4 files changed, 86 insertions(+), 55 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 3a70dacf65..72e5afa558 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -29,26 +29,26 @@ public enum ActionType { ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries - RPC_CALL(null), // log method and params - CREDENTIALS_UPDATED(null), // log new credentials + RPC_CALL(), // log method and params + CREDENTIALS_UPDATED(), // log new credentials ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name - ACTIVATED(null), // log string id - SUSPENDED(null), // log string id + ACTIVATED(), // log string id + SUSPENDED(), // log string id CREDENTIALS_READ(true), // log device id ATTRIBUTES_READ(true), // log attributes RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), RELATION_DELETED(TbMsgType.RELATION_DELETED), RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), - REST_API_RULE_ENGINE_CALL(null), // log call to rule engine from REST API + REST_API_RULE_ENGINE_CALL(), // log call to rule engine from REST API ALARM_ACK(TbMsgType.ALARM_ACK, true), ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), ALARM_DELETE(TbMsgType.ALARM_DELETE, true), ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), - LOGIN(null), - LOGOUT(null), - LOCKOUT(null), + LOGIN(), + LOGOUT(), + LOCKOUT(), ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), @@ -57,33 +57,37 @@ public enum ActionType { UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), ADDED_COMMENT(TbMsgType.COMMENT_CREATED), UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), - DELETED_COMMENT(null), - SMS_SENT(null); + DELETED_COMMENT(), + SMS_SENT(); @Getter - private final boolean isRead; + private final boolean read; private final TbMsgType ruleEngineMsgType; @Getter private final boolean alarmAction; - ActionType(boolean isRead) { - this.isRead = isRead; - this.ruleEngineMsgType = null; - this.alarmAction = false; + ActionType() { + this(false, null, false); + } + + ActionType(boolean read) { + this(read, null, false); } ActionType(TbMsgType ruleEngineMsgType) { - this.isRead = false; - this.ruleEngineMsgType = ruleEngineMsgType; - this.alarmAction = false; + this(false, ruleEngineMsgType, false); } ActionType(TbMsgType ruleEngineMsgType, boolean isAlarmAction) { - this.isRead = false; + this(false, ruleEngineMsgType, isAlarmAction); + } + + ActionType(boolean read, TbMsgType ruleEngineMsgType, boolean alarmAction) { + this.read = read; this.ruleEngineMsgType = ruleEngineMsgType; - this.alarmAction = isAlarmAction; + this.alarmAction = alarmAction; } public Optional getRuleEngineMsgType() { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java index ddfe068937..7006d36b54 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java @@ -17,7 +17,8 @@ package org.thingsboard.server.common.data.audit; import org.junit.jupiter.api.Test; -import java.util.List; +import java.util.EnumSet; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.audit.ActionType.ACTIVATED; @@ -40,7 +41,7 @@ import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED; class ActionTypeTest { - private final List typesWithNullRuleEngineMsgType = List.of( + private final Set typesWithNullRuleEngineMsgType = EnumSet.of( RPC_CALL, CREDENTIALS_UPDATED, ACTIVATED, @@ -55,10 +56,12 @@ class ActionTypeTest { REST_API_RULE_ENGINE_CALL ); - private final List alarmActionTypes = List.of( + private final Set alarmActionTypes = EnumSet.of( ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, ALARM_ASSIGNED, ALARM_UNASSIGNED ); + private final Set readActionTypes = EnumSet.of(CREDENTIALS_READ, ATTRIBUTES_READ); + // backward-compatibility tests @Test @@ -85,4 +88,16 @@ class ActionTypeTest { } } + @Test + void isReadActionTypeTest() { + var types = ActionType.values(); + for (var type : types) { + if (readActionTypes.contains(type)) { + assertThat(type.isRead()).isTrue(); + } else { + assertThat(type.isRead()).isFalse(); + } + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 933909e7e9..b0c6e506b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -123,19 +123,7 @@ public class AuditLogServiceImpl implements AuditLogService { JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; - String entityName = "N/A"; - if (entity != null) { - if (actionType.isAlarmAction()) { - entityName = (entity instanceof AlarmInfo alarmInfo) - ? alarmInfo.getOriginatorName() - : entityService.fetchEntityName(tenantId, entityId).orElse(entityName); - } else { - entityName = entity.getName(); - } - } else try { - entityName = entityService.fetchEntityName(tenantId, entityId).orElse(entityName); - } catch (Exception ignored) { - } + String entityName = getEntityName(tenantId, entityId, entity, actionType); if (e != null) { actionStatus = ActionStatus.FAILURE; failureDetails = getFailureStack(e); @@ -162,6 +150,27 @@ public class AuditLogServiceImpl implements AuditLogService { } } + private String getEntityName(TenantId tenantId, I entityId, E entity, ActionType actionType) { + if (entity == null) { + return fetchEntityName(tenantId, entityId); + } + if (!actionType.isAlarmAction()) { + return entity.getName(); + } + if (entity instanceof AlarmInfo alarmInfo) { + return alarmInfo.getOriginatorName(); + } + return fetchEntityName(tenantId, entityId); + } + + private String fetchEntityName(TenantId tenantId, I entityId) { + try { + return entityService.fetchEntityName(tenantId, entityId).orElse("N/A"); + } catch (Exception ignored) { + return "N/A"; + } + } + private JsonNode constructActionData(I entityId, E entity, ActionType actionType, Object... additionalInfo) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java index 2a876c58db..730fc0e458 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.service.validator.AuditLogDataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; @@ -44,7 +45,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.never; import static org.mockito.BDDMockito.then; @ExtendWith(MockitoExtension.class) @@ -73,15 +73,17 @@ public class AuditLogServiceImplTest { private AuditLogSink auditLogSink; @Test - public void givenEntityIsNull_whenLogEntityAction_thenShouldFetchEntityName() { + public void givenEntityIsNull_whenLogEntityAction_thenShouldFetchEntityName() throws Exception { // GIVEN given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + given(entityService.fetchEntityName(any(), any())).willReturn(Optional.of("Test device")); // WHEN auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, null, ActionType.ADDED, null); // THEN then(entityService).should().fetchEntityName(TENANT_ID, DEVICE_ID); + verifyEntityName("Test device"); } @Test @@ -89,7 +91,7 @@ public class AuditLogServiceImplTest { // GIVEN given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); Alarm alarm = new Alarm(new AlarmId(UUID.fromString("55f577b3-6ef5-4b99-92dc-70eb78b2a970"))); - alarm.setType("Test Alarm"); + alarm.setType("Test alarm"); AlarmComment comment = new AlarmComment(); comment.setComment(JacksonUtil.toJsonNode("{\"comment\": \"test\"}")); @@ -97,13 +99,7 @@ public class AuditLogServiceImplTest { auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, alarm.getId(), alarm, ActionType.ADDED_COMMENT, null, comment); // THEN - then(entityService).should(never()).fetchEntityName(any(), any()); - ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); - then(executor).should().submit(submitTask.capture()); - submitTask.getValue().call(); - ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); - then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); - assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo("Test Alarm"); + verifyEntityName("Test alarm"); } @Test @@ -111,31 +107,38 @@ public class AuditLogServiceImplTest { // GIVEN given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); AlarmInfo alarmInfo = new AlarmInfo(); - alarmInfo.setOriginatorName("Test Device"); + alarmInfo.setOriginatorName("Test device"); // WHEN auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, alarmInfo, ActionType.ALARM_ASSIGNED, null); // THEN - then(entityService).should(never()).fetchEntityName(any(), any()); - ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); - then(executor).should().submit(submitTask.capture()); - submitTask.getValue().call(); - ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); - then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); - assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo("Test Device"); + verifyEntityName("Test device"); } @Test - public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldFetchEntityName() { + public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldFetchEntityName() throws Exception { // GIVEN given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + given(entityService.fetchEntityName(any(), any())).willReturn(Optional.of("Test alarm")); // WHEN auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, new Alarm(), ActionType.ALARM_DELETE, null); // THEN then(entityService).should().fetchEntityName(TENANT_ID, DEVICE_ID); + verifyEntityName("Test alarm"); + } + + private void verifyEntityName(String entityName) throws Exception { + then(auditLogDataValidator).should().validate(any(AuditLog.class), any()); + ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); + then(executor).should().submit(submitTask.capture()); + submitTask.getValue().call(); + ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); + then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); + assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo(entityName); + then(auditLogSink).should().logAction(any()); } } From da277be3065442ee00f371b5069fc58e6df044d4 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 22 Aug 2024 16:31:26 +0300 Subject: [PATCH 275/331] Fixed issue with not saving Widget on Widget editing --- .../home/components/dashboard-page/dashboard-page.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 5af1833794..209b3c1846 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1229,6 +1229,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } else { this.dashboard = dashboard; this.setEditMode(false, false); + this.dashboardConfiguration = this.dashboard.configuration; } }); } From 5032938f49a1e4416483d61356e0556619f7f7db Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 22 Aug 2024 16:38:47 +0300 Subject: [PATCH 276/331] removed brackets --- .../server/common/data/audit/ActionType.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 72e5afa558..dd7a6d98c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -29,26 +29,26 @@ public enum ActionType { ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries - RPC_CALL(), // log method and params - CREDENTIALS_UPDATED(), // log new credentials + RPC_CALL, // log method and params + CREDENTIALS_UPDATED, // log new credentials ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name - ACTIVATED(), // log string id - SUSPENDED(), // log string id + ACTIVATED, // log string id + SUSPENDED, // log string id CREDENTIALS_READ(true), // log device id ATTRIBUTES_READ(true), // log attributes RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), RELATION_DELETED(TbMsgType.RELATION_DELETED), RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), - REST_API_RULE_ENGINE_CALL(), // log call to rule engine from REST API + REST_API_RULE_ENGINE_CALL, // log call to rule engine from REST API ALARM_ACK(TbMsgType.ALARM_ACK, true), ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), ALARM_DELETE(TbMsgType.ALARM_DELETE, true), ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), - LOGIN(), - LOGOUT(), - LOCKOUT(), + LOGIN, + LOGOUT, + LOCKOUT, ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), @@ -57,8 +57,8 @@ public enum ActionType { UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), ADDED_COMMENT(TbMsgType.COMMENT_CREATED), UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), - DELETED_COMMENT(), - SMS_SENT(); + DELETED_COMMENT, + SMS_SENT; @Getter private final boolean read; From 7bf79cfe3dee2298a6b6ccb14d36c245761d15f7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 22 Aug 2024 16:47:54 +0300 Subject: [PATCH 277/331] changed approach to dashboard version change --- .../home/components/dashboard-page/dashboard-page.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 209b3c1846..0d379b1623 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1227,9 +1227,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }; this.init(dashboardPageInitData); } else { - this.dashboard = dashboard; + this.dashboard.version = dashboard.version; this.setEditMode(false, false); - this.dashboardConfiguration = this.dashboard.configuration; } }); } From ddff3b93ccaab69e0d63186d427962e0cea28427 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 22 Aug 2024 16:54:10 +0300 Subject: [PATCH 278/331] UI: update tank --- .../system/scada_symbols/cylindrical-tank.svg | 2 +- .../system/scada_symbols/elevated-tank.svg | 281 +++++++++--------- 2 files changed, 141 insertions(+), 142 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index 293de230cc..1dc50763e4 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -311,7 +311,7 @@ "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", - "default": "##F3F3F3", + "default": "#F3F3F3", "required": null, "subLabel": null, "divider": null, diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index 120604ae95..b3cf71bdda 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -1,12 +1,11 @@ - -{ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="1400" height="2e3" fill="none" version="1.1" viewBox="0 0 1400 2e3"><tb:metadata xmlns=""><![CDATA[{ "title": "Elevated tank", "description": "Elevated tank with current volume value and level visualizations.", "searchTags": [ "tank", "elevator" ], - "widgetSizeX": 6, + "widgetSizeX": 7, "widgetSizeY": 10, "tags": [ { @@ -556,37 +555,8 @@ "step": null } ] -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +}]]> + @@ -638,55 +608,137 @@ - - + + - + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - 1660 gal - - + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -695,7 +747,7 @@ - + @@ -704,7 +756,7 @@ - + @@ -713,7 +765,7 @@ - + @@ -722,7 +774,7 @@ - + @@ -731,7 +783,7 @@ - + @@ -740,7 +792,7 @@ - + @@ -749,7 +801,7 @@ - + @@ -758,7 +810,7 @@ - + @@ -767,7 +819,7 @@ - + @@ -776,7 +828,7 @@ - + @@ -785,7 +837,7 @@ - + @@ -794,7 +846,7 @@ - + @@ -803,7 +855,7 @@ - + @@ -812,7 +864,7 @@ - + @@ -821,7 +873,7 @@ - + @@ -830,7 +882,7 @@ - + @@ -839,7 +891,7 @@ - + @@ -848,42 +900,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1612,22 +1628,5 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file From 36f767a9ce153bbf0f21f56bb428a4cf51eee0ad Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 22 Aug 2024 17:28:45 +0300 Subject: [PATCH 279/331] refactoring --- .../gateway-configuration.component.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts index 386180f1eb..317b616110 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts @@ -25,7 +25,7 @@ import { AttributeService } from '@core/http/attribute.service'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { DeviceService } from '@core/http/device.service'; import { Observable, of, Subject } from 'rxjs'; -import { mergeMap, takeUntil } from 'rxjs/operators'; +import { mergeMap, switchMap, takeUntil } from 'rxjs/operators'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { @@ -92,19 +92,20 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy { saveConfig(): void { const { mode, advancedConfig } = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); - const value = { mode, ...advancedConfig }; + const value = { mode, ...advancedConfig as GatewayConfigValue }; value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands ?? []); const attributes = this.generateAttributes(value); - this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { - this.updateCredentials(value.thingsboard.security).subscribe(() => { - if (this.dialogRef) { - this.dialogRef.close(); - } else { - this.gatewayConfigGroup.markAsPristine(); - this.cd.detectChanges(); - } - }); + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).pipe( + switchMap(_ => this.updateCredentials(value.thingsboard.security)), + takeUntil(this.destroy$), + ).subscribe(() => { + if (this.dialogRef) { + this.dialogRef.close(); + } else { + this.gatewayConfigGroup.markAsPristine(); + this.cd.detectChanges(); + } }); } @@ -216,11 +217,11 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy { } } - private removeEmpty(obj: unknown) { + private removeEmpty(obj: Record): Record { return Object.fromEntries( Object.entries(obj) .filter(([_, v]) => v != null) - .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) + .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v as Record) : v]) ); } From 6df8a97a018208d428ff93518f858c872feebdc8 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 22 Aug 2024 17:52:01 +0300 Subject: [PATCH 280/331] Added trace log level --- .../components/widget/lib/gateway/gateway-widget.models.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 2dde0b8f54..c7f2a35047 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -38,7 +38,8 @@ export enum GatewayLogLevel { ERROR = 'ERROR', WARNING = 'WARNING', INFO = 'INFO', - DEBUG = 'DEBUG' + DEBUG = 'DEBUG', + TRACE = 'TRACE' } export enum PortLimits { From 8d232c64c993e6d05e19c269c001c0e30b4e020b Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 23 Aug 2024 10:21:53 +0300 Subject: [PATCH 281/331] UI: Refactoring --- .../data/json/system/scada_symbols/left-heat-pump.svg | 8 ++++---- .../data/json/system/scada_symbols/right-heat-pump.svg | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg index 1c9fdff63d..1fa5013fcc 100644 --- a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg +++ b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg @@ -48,7 +48,7 @@ "stateRenderFunction": null, "actions": { "click": { - "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var maxTemperature = ctx.properties.maxTemperature;\n var step = ctx.properties.temperatureStep;\n \n var newTemperature = Math.min(maxTemperature, temperature + step);\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" + "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var maxTemperature = ctx.properties.maxTemperature;\n var minTemperature = ctx.properties.minTemperature;\n var step = ctx.properties.temperatureStep;\n var newTemperature = temperature || minTemperature === 0 ? Math.min(maxTemperature, temperature + step) : minTemperature;\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" } } }, @@ -62,7 +62,7 @@ "stateRenderFunction": null, "actions": { "click": { - "actionFunction": "var rinning = ctx.values.running;\nvar action = rinning ? 'stop' : 'run';\n\nif (!ctx.values.temperature) {\n ctx.api.setValue('temperature', ctx.properties.minTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', ctx.properties.minTemperature, {\n error: () => {\n ctx.api.setValue('temperature', ctx.values.temperature);\n }\n });\n}\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('running', !rinning);\n }\n});" + "actionFunction": "var rinning = ctx.values.running;\nvar action = rinning ? 'stop' : 'run';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('running', !rinning);\n }\n});" } } }, @@ -422,7 +422,7 @@ "disableOnProperty": null, "rowClass": "column-xs", "fieldClass": "", - "min": null, + "min": 0, "max": null, "step": 1 }, @@ -438,7 +438,7 @@ "disableOnProperty": null, "rowClass": "", "fieldClass": "", - "min": null, + "min": 0, "max": null, "step": 1 }, diff --git a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg index c6253b9a7a..f4b718eeb4 100644 --- a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg +++ b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg @@ -48,7 +48,7 @@ "stateRenderFunction": null, "actions": { "click": { - "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var maxTemperature = ctx.properties.maxTemperature;\n var step = ctx.properties.temperatureStep;\n \n var newTemperature = Math.min(maxTemperature, temperature + step);\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" + "actionFunction": "if (ctx.values.running) {\n var temperature = ctx.values.temperature; \n var temperature = ctx.values.temperature; \n var maxTemperature = ctx.properties.maxTemperature;\n var minTemperature = ctx.properties.minTemperature;\n var step = ctx.properties.temperatureStep;\n var newTemperature = temperature || minTemperature === 0 ? Math.min(maxTemperature, temperature + step) : minTemperature;\n ctx.api.setValue('temperature', newTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {\n error: () => {\n ctx.api.setValue('temperature', temperature);\n }\n });\n}" } } }, @@ -62,7 +62,7 @@ "stateRenderFunction": null, "actions": { "click": { - "actionFunction": "var rinning = ctx.values.running;\nvar action = rinning ? 'stop' : 'run';\n\nif (!ctx.values.temperature) {\n ctx.api.setValue('temperature', ctx.properties.minTemperature);\n ctx.api.callAction(event, 'updateTemperatureState', ctx.properties.minTemperature, {\n error: () => {\n ctx.api.setValue('temperature', ctx.values.temperature);\n }\n });\n}\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('running', !rinning);\n }\n});" + "actionFunction": "var rinning = ctx.values.running;\nvar action = rinning ? 'stop' : 'run';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('running', !rinning);\n }\n});" } } }, @@ -422,7 +422,7 @@ "disableOnProperty": null, "rowClass": "column-xs", "fieldClass": "", - "min": null, + "min": 0, "max": null, "step": 1 }, @@ -438,7 +438,7 @@ "disableOnProperty": null, "rowClass": "", "fieldClass": "", - "min": null, + "min": 0, "max": null, "step": 1 }, From a59552dc1d90d5aa891dc0bd8b7fba43fe3c7c46 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 23 Aug 2024 12:16:51 +0300 Subject: [PATCH 282/331] fixed tests in alarm controller --- .../thingsboard/server/controller/AlarmControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 7ae283cd46..2f264d49ff 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -260,7 +260,7 @@ public class AlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), - tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_DELETE); + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_DELETE, alarm.getId()); } @Test @@ -273,7 +273,7 @@ public class AlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_DELETE); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_DELETE, alarm.getId()); } @Test From f003582f1473f90fa282f740e3dac8f73cf4e13f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 23 Aug 2024 12:53:12 +0300 Subject: [PATCH 283/331] UI: scada symbol replace for unique ids --- .../scada_symbols/bottom-flow-meter.svg | 2 +- .../scada_symbols/bottom-right-elbow-pipe.svg | 2 +- .../system/scada_symbols/bottom-tee-pipe.svg | 2 +- .../json/system/scada_symbols/cross-pipe.svg | 2 +- .../horizontal-inline-flow-meter.svg | 2 +- .../system/scada_symbols/horizontal-pipe.svg | 5 +-- .../scada_symbols/left-bottom-elbow-pipe.svg | 8 ++-- .../system/scada_symbols/left-flow-meter.svg | 2 +- .../system/scada_symbols/left-tee-pipe.svg | 2 +- .../scada_symbols/left-top-elbow-pipe.svg | 8 ++-- .../scada_symbols/long-horizontal-pipe.svg | 2 +- .../scada_symbols/long-vertical-pipe.svg | 2 +- .../system/scada_symbols/right-flow-meter.svg | 2 +- .../system/scada_symbols/right-tee-pipe.svg | 2 +- .../system/scada_symbols/top-flow-meter.svg | 5 +-- .../scada_symbols/top-right-elbow-pipe.svg | 8 ++-- .../system/scada_symbols/top-tee-pipe.svg | 2 +- .../vertical-inline-flow-meter.svg | 5 +-- .../system/scada_symbols/vertical-pipe.svg | 5 +-- .../widget/lib/scada/scada-symbol.models.ts | 37 ++++++++++++++----- 20 files changed, 59 insertions(+), 46 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg index 93c496f2e3..24927d595e 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg @@ -38,7 +38,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = element.reference('fill');\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg index b3f6488f3e..4dc6074864 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg @@ -8,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i - +}]]> diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg index cd9b5f1b7a..6179153171 100644 --- a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg @@ -1,4 +1,5 @@ -{ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"> +<tb:metadata xmlns=""><![CDATA[{ "title": "Left bottom elbow pipe", "description": "Left bottom elbow pipe with fluid and leak visualizations.", "searchTags": [ @@ -7,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(-delta, delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-fluid');\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(fluidElementTag) {\n return ctx.tags[fluidElementTag][0].reference('fill');\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(-delta, delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", "tags": [ { "tag": "center-fluid", @@ -250,8 +251,7 @@ "step": null } ] -} - +}]]> diff --git a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg index b1da702871..9ea330b68d 100644 --- a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg @@ -38,7 +38,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = element.reference('fill');\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg index a0e01f639b..d124709a68 100644 --- a/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg @@ -8,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var leftLiquidPattern = prepareLiquidPattern('left-liquid', 'left-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\nvar bottomLiquidPattern = prepareLiquidPattern('bottom-liquid', 'bottom-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n leftLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n bottomLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(leftLiquidPattern, 'left');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\nupdateLiquidPatternAnimation(bottomLiquidPattern, 'bottom');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i{ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"> +<tb:metadata xmlns=""><![CDATA[{ "title": "Left top elbow pipe", "description": "Left top elbow pipe with fluid and leak visualizations.", "searchTags": [ @@ -7,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(delta, -delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-fluid');\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(fluidElementTag) {\n return ctx.tags[fluidElementTag][0].reference('fill');\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(delta, -delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", "tags": [ { "tag": "center-fluid", @@ -250,8 +251,7 @@ "step": null } ] -} - +}]]> diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg index ffc77b0ac0..bd3b223251 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg @@ -11,7 +11,7 @@ "tags": [ { "tag": "fluid", - "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = element.reference('fill');\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg index 7f4e30edad..769ae94270 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg @@ -11,7 +11,7 @@ "tags": [ { "tag": "fluid", - "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = element.reference('fill');\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg index e5df51e561..11684d2cb8 100644 --- a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg @@ -38,7 +38,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = element.reference('fill');\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg index 4c84999b1f..fce69fa7a3 100644 --- a/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg @@ -8,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var rightLiquidPattern = prepareLiquidPattern('right-liquid', 'right-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\nvar bottomLiquidPattern = prepareLiquidPattern('bottom-liquid', 'bottom-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n rightLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n bottomLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(rightLiquidPattern, 'right');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\nupdateLiquidPatternAnimation(bottomLiquidPattern, 'bottom');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i - +}]]> diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg index 5537c23bb6..89beca4c85 100644 --- a/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg @@ -1,4 +1,5 @@ -{ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"> +<tb:metadata xmlns=""><![CDATA[{ "title": "Top right elbow pipe", "description": "Top right elbow pipe with fluid and leak visualizations.", "searchTags": [ @@ -7,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(delta, -delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-fluid');\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(fluidElementTag) {\n return ctx.tags[fluidElementTag][0].reference('fill');\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(delta, -delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", "tags": [ { "tag": "center-fluid", @@ -250,8 +251,7 @@ "step": null } ] -} - +}]]> diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg index 1dfa27e706..96199571ef 100644 --- a/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg @@ -8,7 +8,7 @@ ], "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "var leftLiquidPattern = prepareLiquidPattern('left-liquid', 'left-fluid');\nvar rightLiquidPattern = prepareLiquidPattern('right-liquid', 'right-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n leftLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n rightLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(leftLiquidPattern, 'left');\nupdateLiquidPatternAnimation(rightLiquidPattern, 'right');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i - +}]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg index 18ea6837e0..3f6d912459 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg @@ -11,7 +11,7 @@ "tags": [ { "tag": "fluid", - "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = element.reference('fill');\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", "actions": null }, { @@ -240,8 +240,7 @@ "step": null } ] -}]]> - +}]]> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 8beac3fb32..3c4e32abf7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -204,6 +204,16 @@ const svgPartsRegex = /()(.*)<\/svg>/gms; const tbNamespaceRegex = //gms; +const generateElementId = () => { + const id = guid(); + const firstChar = id.charAt(0); + if (firstChar >= '0' && firstChar <= '9') { + return 'a' + id; + } else { + return id; + } +}; + export const applyTbNamespaceToSvgContent = (svgContent: string): string => { svgPartsRegex.lastIndex = 0; let svgRootNode: string; @@ -536,7 +546,22 @@ export class ScadaSymbolObject { for (let i=0;i { - const id = guid(); - const firstChar = id.charAt(0); - if (firstChar >= '0' && firstChar <= '9') { - return 'a' + id; - } else { - return id; - } - }, + generateElementId: () => generateElementId(), formatValue, text: this.setElementText.bind(this), font: this.setElementFont.bind(this), From c16e11d66472983b0cd7bb829c95c24f6a5aef3b Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 23 Aug 2024 15:37:49 +0300 Subject: [PATCH 284/331] UI: Refactoring tanks --- .../main/data/json/system/scada_symbols/cylindrical-tank.svg | 2 +- .../src/main/data/json/system/scada_symbols/elevated-tank.svg | 2 +- .../src/main/data/json/system/scada_symbols/horizontal-tank.svg | 2 +- .../data/json/system/scada_symbols/large-cylindrical-tank.svg | 2 +- .../json/system/scada_symbols/large-stand-cylindrical-tank.svg | 2 +- .../json/system/scada_symbols/large-stand-vertical-tank.svg | 2 +- .../main/data/json/system/scada_symbols/large-vertical-tank.svg | 2 +- application/src/main/data/json/system/scada_symbols/pool.svg | 2 +- .../data/json/system/scada_symbols/small-spherical-tank.svg | 2 +- .../src/main/data/json/system/scada_symbols/spherical-tank.svg | 2 +- .../data/json/system/scada_symbols/stand-cylindrical-tank.svg | 2 +- .../data/json/system/scada_symbols/stand-horizontal-tank.svg | 2 +- .../json/system/scada_symbols/stand-vertical-short-tank.svg | 2 +- .../main/data/json/system/scada_symbols/stand-vertical-tank.svg | 2 +- .../main/data/json/system/scada_symbols/vertical-short-tank.svg | 2 +- .../src/main/data/json/system/scada_symbols/vertical-tank.svg | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index 1dc50763e4..104b7bc864 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index b3cf71bdda..7874e627df 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*895; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*895; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg index c504f0303b..02832fdce1 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg index 4773147dff..33dcc947ba 100644 --- a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index ef365ab465..89b72339c2 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*910; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index d38164a814..7e5170b582 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg index 0387e160ad..5958836557 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*763; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/pool.svg b/application/src/main/data/json/system/scada_symbols/pool.svg index a0eab2be0b..86bf0d5907 100644 --- a/application/src/main/data/json/system/scada_symbols/pool.svg +++ b/application/src/main/data/json/system/scada_symbols/pool.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*740; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.transparent) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*740; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg index b42fd2ab5b..0acbbc9f38 100644 --- a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*560; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*560; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index cf11b61bba..069c54f444 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*960; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*960; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index f9948bc64b..2b3b3f010f 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*900; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg index 9adfa3fb82..bea10fd317 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*568; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index 5c181e46d9..f9dcb3b141 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -26,7 +26,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 245});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 245 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 245});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 245 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg index e7d14a9f23..078f58d810 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg index 662bdd1bb2..1b1e825d48 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -25,7 +25,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 245});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 245 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 245});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*442; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 245 + height });\n }\n}\n", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index 258cb590d0..5c3a4561db 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -24,7 +24,7 @@ }, { "tag": "fluid", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = element.reference('fill');\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", "actions": null }, { From bc7f4b3333e2ca6c58f194ca72656ccfdb530df0 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 23 Aug 2024 18:01:13 +0300 Subject: [PATCH 285/331] UI: Menu service: minor fix --- ui-ngx/src/app/core/services/menu.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 90976e282c..42668c7612 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -72,10 +72,12 @@ export class MenuService { private updateOpenedMenuSections() { const url = this.router.url; const openedMenuSections = getCurrentOpenedMenuSections(this.store); - this.currentMenuSections.filter(section => section.type === 'toggle' && - (url.startsWith(section.path) || openedMenuSections.includes(section.path))).forEach( - section => section.opened = true - ); + if (this.currentMenuSections?.length) { + this.currentMenuSections.filter(section => section.type === 'toggle' && + (url.startsWith(section.path) || openedMenuSections.includes(section.path))).forEach( + section => section.opened = true + ); + } } private allMenuLinks(sections: Array): Array { From 6f833a3b18849e8af10c6517692b0c41b2b37f14 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 26 Aug 2024 10:51:13 +0300 Subject: [PATCH 286/331] minor refactoring --- .../server/service/entitiy/domain/DefaultTbDomainService.java | 2 +- .../service/entitiy/mobile/DefaultTbMobileAppService.java | 2 +- .../java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java | 2 +- .../thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java | 2 +- .../thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java index 59cf6f9856..dc876c2cfa 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java @@ -42,7 +42,7 @@ public class DefaultTbDomainService extends AbstractTbEntityService implements T TenantId tenantId = domain.getTenantId(); try { Domain savedDomain = checkNotNull(domainService.saveDomain(tenantId, domain)); - if (!CollectionUtils.isEmpty(oAuth2Clients)) { + if (CollectionUtils.isNotEmpty(oAuth2Clients)) { domainService.updateOauth2Clients(domain.getTenantId(), savedDomain.getId(), oAuth2Clients); } logEntityActionService.logEntityAction(tenantId, savedDomain.getId(), savedDomain, actionType, user, oAuth2Clients); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java index 66da7341cc..8dbef1fc1e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java @@ -42,7 +42,7 @@ public class DefaultTbMobileAppService extends AbstractTbEntityService implement TenantId tenantId = mobileApp.getTenantId(); try { MobileApp savedMobileApp = checkNotNull(mobileAppService.saveMobileApp(tenantId, mobileApp)); - if (!CollectionUtils.isEmpty(oauth2Clients)) { + if (CollectionUtils.isNotEmpty(oauth2Clients)) { mobileAppService.updateOauth2Clients(tenantId, savedMobileApp.getId(), oauth2Clients); } logEntityActionService.logEntityAction(tenantId, savedMobileApp.getId(), savedMobileApp, actionType, user); diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java index 5880db100f..251e47bc8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java @@ -32,7 +32,7 @@ public interface OAuth2ClientDao extends Dao { List findEnabledByDomainName(String domainName); - List findEnabledByPckNameAndPlatformType(String pkgName, PlatformType platformType); + List findEnabledByPkgNameAndPlatformType(String pkgName, PlatformType platformType); List findByDomainId(UUID domainId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index af0a881e49..843ed4d207 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -61,7 +61,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public List findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(String pkgName, PlatformType platformType) { log.trace("Executing findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType pkgName=[{}] platformType=[{}]",pkgName, platformType); - return oauth2ClientDao.findEnabledByPckNameAndPlatformType(pkgName, platformType) + return oauth2ClientDao.findEnabledByPkgNameAndPlatformType(pkgName, platformType) .stream() .map(OAuth2Utils::toClientLoginInfo) .collect(Collectors.toList()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java index cd36da43cf..0c09453e08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java @@ -64,7 +64,7 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao findEnabledByPckNameAndPlatformType(String pkgName, PlatformType platformType) { + public List findEnabledByPkgNameAndPlatformType(String pkgName, PlatformType platformType) { return DaoUtil.convertDataList(repository.findEnabledByPkgNameAndPlatformType(pkgName, platformType != null ? platformType.name() : null)); } From c9b46f2e02cf885eb87e0993bfce4dc3f5a89756 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 26 Aug 2024 10:55:41 +0300 Subject: [PATCH 287/331] minor refactoring --- .../org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java | 2 +- .../server/dao/oauth2/OAuth2ClientServiceImpl.java | 2 +- .../thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java index 251e47bc8f..e4bfcce4c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java @@ -42,5 +42,5 @@ public interface OAuth2ClientDao extends Dao { void deleteByTenantId(UUID tenantId); - List findByIds(TenantId tenantId, List oAuth2ClientIds); + List findByIds(UUID tenantId, List oAuth2ClientIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index 843ed4d207..d0af6b22d0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -123,7 +123,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public List findOAuth2ClientInfosByIds(TenantId tenantId, List oAuth2ClientIds) { log.trace("Executing findQueueStatsByIds, tenantId [{}], oAuth2ClientIds [{}]", tenantId, oAuth2ClientIds); - return oauth2ClientDao.findByIds(tenantId, oAuth2ClientIds) + return oauth2ClientDao.findByIds(tenantId.getId(), oAuth2ClientIds) .stream() .map(OAuth2ClientInfo::new) .collect(Collectors.toList()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java index 0c09453e08..c8f16cf1c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java @@ -90,8 +90,8 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao findByIds(TenantId tenantId, List oAuth2ClientIds) { - return DaoUtil.convertDataList(repository.findByTenantIdAndIdIn(tenantId.getId(), toUUIDs(oAuth2ClientIds))); + public List findByIds(UUID tenantId, List oAuth2ClientIds) { + return DaoUtil.convertDataList(repository.findByTenantIdAndIdIn(tenantId, toUUIDs(oAuth2ClientIds))); } @Override From 0ab35b72d2f10a6f069629bc348517a25bb604e3 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 26 Aug 2024 14:51:36 +0300 Subject: [PATCH 288/331] Edge: impl new approach with oauth --- .../edge/DefaultEdgeNotificationService.java | 2 +- .../service/edge/EdgeContextComponent.java | 4 +- .../edge/EdgeEventSourcingListener.java | 13 +- .../service/edge/rpc/EdgeGrpcSession.java | 4 +- .../service/edge/rpc/EdgeSyncCursor.java | 2 +- .../oauth2/OAuth2MsgConstructor.java | 34 ++++- .../rpc/fetch/OAuth2EdgeEventFetcher.java | 29 ++-- .../edge/rpc/processor/BaseEdgeProcessor.java | 4 + .../processor/device/DeviceEdgeProcessor.java | 19 +-- .../profile/DeviceProfileEdgeProcessor.java | 7 +- .../processor/oauth2/OAuth2EdgeProcessor.java | 94 +++++++++--- .../queue/DefaultTbClusterService.java | 8 +- .../queue/DefaultTbCoreConsumerService.java | 8 - .../server/controller/EdgeControllerTest.java | 6 +- .../server/edge/AbstractEdgeTest.java | 19 +-- .../server/edge/OAuth2EdgeTest.java | 144 ++++++++++++++---- .../server/edge/imitator/EdgeImitator.java | 14 +- .../dao/oauth2/OAuth2ClientService.java | 3 + .../common/data/edge/EdgeEventType.java | 3 +- .../common/data/id/EntityIdFactory.java | 4 + .../thingsboard/edge/rpc/EdgeGrpcClient.java | 2 +- common/edge-api/src/main/proto/edge.proto | 19 ++- .../server/dao/domain/DomainServiceImpl.java | 4 +- .../server/dao/oauth2/OAuth2ClientDao.java | 3 + .../dao/oauth2/OAuth2ClientServiceImpl.java | 8 +- .../server/dao/sql/domain/JpaDomainDao.java | 2 +- .../dao/sql/oauth2/JpaOAuth2ClientDao.java | 5 + .../sql/oauth2/OAuth2ClientRepository.java | 5 + 28 files changed, 319 insertions(+), 150 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index d7b6db7f21..740b19e5d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -201,7 +201,7 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case OAUTH2_CLIENT -> oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg); + case DOMAIN, OAUTH2_CLIENT -> oAuth2EdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 8e609ed545..2d9fa01796 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -29,13 +29,13 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.resource.ResourceService; @@ -167,7 +167,7 @@ public class EdgeContextComponent { private NotificationTemplateService notificationTemplateService; @Autowired - private OAuth2ClientService oAuth2ClientService; + private DomainService domainService; @Autowired private RateLimitService rateLimitService; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index b0b1506e6e..f36f9ee2c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -32,10 +32,10 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -204,11 +204,12 @@ public class EdgeEventSourcingListener { return !event.getCreated(); case API_USAGE_STATE, EDGE: return false; + case DOMAIN: + if (entity instanceof Domain domain) { + return domain.isPropagateToEdge(); + } } } -// if (entity instanceof OAuth2Info oAuth2Info) { -// return oAuth2Info.isEdgeEnabled(); -// } // Default: If the entity doesn't match any of the conditions, consider it as valid. return true; } @@ -233,8 +234,6 @@ public class EdgeEventSourcingListener { private EdgeEventType getEdgeEventTypeForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return EdgeEventType.ALARM_COMMENT; - } else if (entity instanceof OAuth2Client) { - return EdgeEventType.OAUTH2_CLIENT; } return null; } @@ -242,8 +241,6 @@ public class EdgeEventSourcingListener { private String getBodyMsgForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return JacksonUtil.toString(entity); - } else if (entity instanceof OAuth2Client) { - return JacksonUtil.toString(entity); } return null; } 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 0119d15a3a..c4e8f1ec7c 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 @@ -699,7 +699,9 @@ public final class EdgeGrpcSession implements Closeable { case NOTIFICATION_TEMPLATE: return ctx.getNotificationEdgeProcessor().convertNotificationTemplateToDownlink(edgeEvent); case OAUTH2_CLIENT: - return ctx.getOAuth2EdgeProcessor().convertOAuth2ProviderEventToDownlink(edgeEvent); + return ctx.getOAuth2EdgeProcessor().convertOAuth2ClientEventToDownlink(edgeEvent, this.edgeVersion); + case DOMAIN: + return ctx.getOAuth2EdgeProcessor().convertOAuth2DomainEventToDownlink(edgeEvent, this.edgeVersion); default: log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent); return null; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 504567506a..7426207a56 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -88,7 +88,7 @@ public class EdgeSyncCursor { fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); - fetchers.add(new OAuth2EdgeEventFetcher(ctx.getOAuth2ClientService())); + fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java index 3c3f1a378c..7ed25924b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java @@ -17,16 +17,44 @@ package org.thingsboard.server.service.edge.rpc.constructor.oauth2; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent public class OAuth2MsgConstructor { - public OAuth2UpdateMsg constructOAuth2UpdateMsg(OAuth2Client oAuth2Client) { - return OAuth2UpdateMsg.newBuilder().setEntity(JacksonUtil.toString(oAuth2Client)).build(); + public OAuth2ClientUpdateMsg constructOAuth2ClientUpdateMsg(UpdateMsgType msgType, OAuth2Client oAuth2Client) { + return OAuth2ClientUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(oAuth2Client)) + .setIdMSB(oAuth2Client.getId().getId().getMostSignificantBits()) + .setIdLSB(oAuth2Client.getId().getId().getLeastSignificantBits()).build(); + } + + public OAuth2ClientUpdateMsg constructOAuth2ClientDeleteMsg(OAuth2ClientId oAuth2ClientId) { + return OAuth2ClientUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(oAuth2ClientId.getId().getMostSignificantBits()) + .setIdLSB(oAuth2ClientId.getId().getLeastSignificantBits()).build(); + } + + public OAuth2DomainUpdateMsg constructOAuth2DomainUpdateMsg(UpdateMsgType msgType, DomainInfo domainInfo) { + return OAuth2DomainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(domainInfo)) + .setIdMSB(domainInfo.getId().getId().getMostSignificantBits()) + .setIdLSB(domainInfo.getId().getId().getLeastSignificantBits()).build(); + } + + public OAuth2DomainUpdateMsg constructOAuth2DomainDeleteMsg(DomainId domainId) { + return OAuth2DomainUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(domainId.getId().getMostSignificantBits()) + .setIdLSB(domainId.getId().getLeastSignificantBits()) + .build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java index 3cef159b2f..c93135be37 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java @@ -17,43 +17,32 @@ package org.thingsboard.server.service.edge.rpc.fetch; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.oauth2.OAuth2ClientService; - -import java.util.ArrayList; -import java.util.List; +import org.thingsboard.server.dao.domain.DomainService; @AllArgsConstructor @Slf4j -public class OAuth2EdgeEventFetcher implements EdgeEventFetcher { +public class OAuth2EdgeEventFetcher extends BasePageableEdgeEventFetcher { - private final OAuth2ClientService oAuth2ClientService; + private final DomainService domainService; @Override - public PageLink getPageLink(int pageSize) { - return null; + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return domainService.findDomainInfosByTenantId(TenantId.SYS_TENANT_ID, pageLink); } @Override - public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { -// OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info(); -// if (!oAuth2Info.isEdgeEnabled()) { -// return new PageData<>(); -// } - List result = new ArrayList<>(); -// result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2, -// EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info))); - // returns PageData object to be in sync with other fetchers - return new PageData<>(result, 1, result.size(), false); + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, DomainInfo domainInfo) { + return EdgeUtils.constructEdgeEvent(TenantId.SYS_TENANT_ID, edge.getId(), EdgeEventType.DOMAIN, + EdgeEventActionType.ADDED, domainInfo.getId(), null); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 66180f40a2..bb1d6383e1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -69,6 +69,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; @@ -242,6 +243,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected OAuth2ClientService oAuth2ClientService; + @Autowired + protected DomainService domainService; + @Autowired @Lazy protected TbQueueProducerProvider producerProvider; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index f448766994..a55dd77e01 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -216,6 +216,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED: case UPDATED: @@ -233,24 +234,20 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements .addDeviceUpdateMsg(deviceUpdateMsg); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); - builder.addDeviceProfileUpdateMsg(((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); + builder.addDeviceProfileUpdateMsg(msgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); } downlinkMsg = builder.build(); } break; case DELETED: case UNASSIGNED_FROM_EDGE: - DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceDeleteMsg(deviceId); + DeviceUpdateMsg deviceUpdateMsg = msgConstructor.constructDeviceDeleteMsg(deviceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg) @@ -259,8 +256,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements case CREDENTIALS_UPDATED: DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg) @@ -270,11 +266,10 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements case RPC_CALL: return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceRpcCallMsg(((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) + .addDeviceRpcCallMsg(msgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) .build(); } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 464fa3ba54..f547cbc3a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -96,14 +96,14 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = msgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) @@ -111,8 +111,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces } } case DELETED -> { - DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileDeleteMsg(deviceProfileId); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = msgConstructor.constructDeviceProfileDeleteMsg(deviceProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java index 9daf428df6..999defcd02 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java @@ -15,49 +15,95 @@ */ package org.thingsboard.server.service.edge.rpc.processor.oauth2; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; @Slf4j @Component @TbCoreComponent public class OAuth2EdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertOAuth2ProviderEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertOAuth2DomainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) { + return null; + } + DomainId domainId = new DomainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; -// OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class); -// if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) { -// OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info); -// downlinkMsg = DownlinkMsg.newBuilder() -// .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) -// .addOAuth2UpdateMsg(oAuth2ProviderUpdateMsg) -// .build(); -// } + + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + DomainInfo domainInfo = domainService.findDomainInfoById(edgeEvent.getTenantId(), domainId); + if (domainInfo != null && domainInfo.isPropagateToEdge()) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = oAuth2MsgConstructor.constructOAuth2DomainUpdateMsg(msgType, domainInfo); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2DomainUpdateMsg(oAuth2DomainUpdateMsg); + domainInfo.getOauth2ClientInfos().forEach(clientInfo -> { + OAuth2Client oauth2Client = oAuth2ClientService.findOAuth2ClientById(edgeEvent.getTenantId(), clientInfo.getId()); + OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = oAuth2MsgConstructor.constructOAuth2ClientUpdateMsg(msgType, oauth2Client); + builder.addOAuth2ClientUpdateMsg(oAuth2ClientUpdateMsg); + }); + downlinkMsg = builder.build(); + } + } + case DELETED -> { + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = oAuth2MsgConstructor.constructOAuth2DomainDeleteMsg(domainId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2DomainUpdateMsg(oAuth2DomainUpdateMsg) + .build(); + } + } return downlinkMsg; } - public ListenableFuture processOAuth2Notification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - OAuth2Client oAuth2Info = JacksonUtil.fromString(edgeNotificationMsg.getBody(), OAuth2Client.class); - if (oAuth2Info == null) { - return Futures.immediateFuture(null); + public DownlinkMsg convertOAuth2ClientEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) { + return null; } - EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); - return processActionForAllEdges(tenantId, type, actionType, null, JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()), null); + OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + boolean isPropagateToEdge = oAuth2ClientService.isPropagateOAuth2ClientToEdge(edgeEvent.getTenantId(), oAuth2ClientId); + if (!isPropagateToEdge) { + return null; + } + OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(edgeEvent.getTenantId(), oAuth2ClientId); + if (oAuth2Client != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = oAuth2MsgConstructor.constructOAuth2ClientUpdateMsg(msgType, oAuth2Client); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2ClientUpdateMsg(oAuth2ClientUpdateMsg) + .build(); + } + } + case DELETED -> { + OAuth2ClientUpdateMsg oAuth2ClientDeleteMsg = oAuth2MsgConstructor.constructOAuth2ClientDeleteMsg(oAuth2ClientId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2ClientUpdateMsg(oAuth2ClientDeleteMsg) + .build(); + } + } + return downlinkMsg; } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 70a39f0851..7699a4f7c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -614,13 +614,11 @@ public class DefaultTbClusterService implements TbClusterService { private void pushDeviceUpdateMessage(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { log.trace("{} Going to send edge update notification for device actor, device id {}, edge id {}", tenantId, entityId, edgeId); switch (action) { - case ASSIGNED_TO_EDGE: - pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), edgeId), null); - break; - case UNASSIGNED_FROM_EDGE: + case ASSIGNED_TO_EDGE -> pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), edgeId), null); + case UNASSIGNED_FROM_EDGE -> { EdgeId relatedEdgeId = findRelatedEdgeIdIfAny(tenantId, entityId); pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), relatedEdgeId), null); - break; + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 1dc42218fb..4462183baa 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -108,7 +108,6 @@ import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpd import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -701,13 +700,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg, TbCallback callback) { - if (actorMsg.isPresent()) { - forwardToAppActor(id, actorMsg.get()); - } - callback.onSuccess(); - } - private void forwardToAppActor(UUID id, TbActorMsg actorMsg) { log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); actorContext.tell(actorMsg); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index de530b5268..b636849774 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -81,7 +81,8 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; @@ -892,7 +893,8 @@ public class EdgeControllerTest extends AbstractControllerTest { EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); - edgeImitator.ignoreType(OAuth2UpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); edgeImitator.expectMessageAmount(27); edgeImitator.connect(); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 2c60e1093a..bd8a59a975 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -63,7 +63,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; @@ -87,7 +86,8 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; @@ -144,7 +144,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.ignoreType(OAuth2UpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); edgeImitator.expectMessageAmount(24); edgeImitator.connect(); @@ -547,18 +548,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(customer.isPublic()); } - private void validateOAuth2() throws Exception { - Optional oAuth2UpdateMsgOpt = edgeImitator.findMessageByType(OAuth2UpdateMsg.class); - Assert.assertTrue(oAuth2UpdateMsgOpt.isPresent()); - OAuth2UpdateMsg oAuth2ProviderUpdateMsg = oAuth2UpdateMsgOpt.get(); - OAuth2Client oAuth2Client = JacksonUtil.fromString(oAuth2ProviderUpdateMsg.getEntity(), OAuth2Client.class, true); - Assert.assertNotNull(oAuth2Client); - OAuth2Client auth2Info = doGet("/api/oauth2/config", OAuth2Client.class); - Assert.assertNotNull(auth2Info); - Assert.assertEquals(oAuth2Client, auth2Info); - testAutoGeneratedCodeByProtobuf(oAuth2ProviderUpdateMsg); - } - private void validateSyncCompleted() { Optional syncCompletedMsgOpt = edgeImitator.findMessageByType(SyncCompletedMsg.class); Assert.assertTrue(syncCompletedMsgOpt.isPresent()); diff --git a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java index bb9073298d..10c6b245a6 100644 --- a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java @@ -20,55 +20,128 @@ import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.util.Arrays; import java.util.Collections; +import java.util.Optional; import java.util.UUID; @DaoSqlTest -@Ignore("Ignored till fixed") public class OAuth2EdgeTest extends AbstractEdgeTest { @Test - public void testOAuth2Support() throws Exception { + public void testOAuth2DomainSupport() throws Exception { loginSysAdmin(); - // enable oauth + // enable oauth and save domain edgeImitator.allowIgnoredTypes(); edgeImitator.expectMessageAmount(1); - OAuth2Client oAuth2Client = createDefaultOAuth2Info(); - oAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + Domain savedDomain = doPost("/api/domain", constructDomain(), Domain.class); Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg); - OAuth2UpdateMsg oAuth2ProviderUpdateMsg = (OAuth2UpdateMsg) latestMessage; - OAuth2Client result = JacksonUtil.fromString(oAuth2ProviderUpdateMsg.getEntity(), OAuth2Client.class, true); - Assert.assertEquals(oAuth2Client, result); + Assert.assertTrue(latestMessage instanceof OAuth2DomainUpdateMsg); + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = (OAuth2DomainUpdateMsg) latestMessage; + Domain result = JacksonUtil.fromString(oAuth2DomainUpdateMsg.getEntity(), Domain.class, true); + Assert.assertEquals(savedDomain, result); + + // disable oauth support: no update of domain events is sending to Edge + edgeImitator.expectMessageAmount(1); + savedDomain.setPropagateToEdge(false); + doPost("/api/domain", savedDomain, Domain.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); - // disable oauth support + // delete domain edgeImitator.expectMessageAmount(1); -// oAuth2Info.setEnabled(false); -// oAuth2Info.setEdgeEnabled(false); -// doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); -// Assert.assertFalse(edgeImitator.waitForMessages(5)); + doDelete("/api/domain/" + savedDomain.getId().getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2DomainUpdateMsg); + oAuth2DomainUpdateMsg = (OAuth2DomainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, oAuth2DomainUpdateMsg.getMsgType()); + Assert.assertEquals(savedDomain.getUuidId().getMostSignificantBits(), oAuth2DomainUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDomain.getUuidId().getLeastSignificantBits(), oAuth2DomainUpdateMsg.getIdLSB()); - edgeImitator.ignoreType(OAuth2UpdateMsg.class); + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); loginTenantAdmin(); } - private OAuth2Client createDefaultOAuth2Info() { - return validRegistrationInfo(); + @Test + public void testOAuth2ClientSupport() throws Exception { + loginSysAdmin(); + + // enable oauth and save domain + edgeImitator.allowIgnoredTypes(); + + edgeImitator.expectMessageAmount(2); + OAuth2Client savedOAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test edge google client"); + savedOAuth2Client = doPost("/api/oauth2/client", savedOAuth2Client, OAuth2Client.class); + Domain savedDomain = doPost("/api/domain?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), constructDomain(), Domain.class); + + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional oAuth2DomainUpdateMsgOpt = edgeImitator.findMessageByType(OAuth2DomainUpdateMsg.class); + Assert.assertTrue(oAuth2DomainUpdateMsgOpt.isPresent()); + Domain result = JacksonUtil.fromString(oAuth2DomainUpdateMsgOpt.get().getEntity(), Domain.class, true); + Assert.assertEquals(savedDomain, result); + + Optional oAuth2ClientUpdateMsgOpt = edgeImitator.findMessageByType(OAuth2ClientUpdateMsg.class); + Assert.assertTrue(oAuth2ClientUpdateMsgOpt.isPresent()); + OAuth2Client clientResult = JacksonUtil.fromString(oAuth2ClientUpdateMsgOpt.get().getEntity(), OAuth2Client.class, true); + Assert.assertEquals(savedOAuth2Client, clientResult); + + // disable oauth support: no update of domain events and client events are sending to Edge + edgeImitator.expectMessageAmount(1); + savedDomain.setPropagateToEdge(false); + doPost("/api/domain", savedDomain, Domain.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + edgeImitator.expectMessageAmount(1); + savedOAuth2Client.setTitle("Updated title"); + doPost("/api/oauth2/client", savedOAuth2Client, OAuth2Client.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + // delete oauth2Client + edgeImitator.expectMessageAmount(1); + doDelete("/api/oauth2/client/" + savedOAuth2Client.getId().getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2ClientUpdateMsg); + OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = (OAuth2ClientUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, oAuth2ClientUpdateMsg.getMsgType()); + Assert.assertEquals(savedOAuth2Client.getUuidId().getMostSignificantBits(), oAuth2ClientUpdateMsg.getIdMSB()); + Assert.assertEquals(savedOAuth2Client.getUuidId().getLeastSignificantBits(), oAuth2ClientUpdateMsg.getIdLSB()); + + // delete domain + edgeImitator.expectMessageAmount(1); + doDelete("/api/domain/" + savedDomain.getId().getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2DomainUpdateMsg); + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = (OAuth2DomainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, oAuth2DomainUpdateMsg.getMsgType()); + Assert.assertEquals(savedDomain.getUuidId().getMostSignificantBits(), oAuth2DomainUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDomain.getUuidId().getLeastSignificantBits(), oAuth2DomainUpdateMsg.getIdLSB()); + + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); + loginTenantAdmin(); } - private OAuth2Client validRegistrationInfo() { + private OAuth2Client validClientInfo(TenantId tenantId, String title) { OAuth2Client oAuth2Client = new OAuth2Client(); - oAuth2Client.setTitle(UUID.randomUUID().toString()); + oAuth2Client.setTenantId(tenantId); + oAuth2Client.setTitle(title); oAuth2Client.setClientId(UUID.randomUUID().toString()); oAuth2Client.setClientSecret(UUID.randomUUID().toString()); oAuth2Client.setAuthorizationUri(UUID.randomUUID().toString()); @@ -80,16 +153,29 @@ public class OAuth2EdgeTest extends AbstractEdgeTest { oAuth2Client.setJwkSetUri(UUID.randomUUID().toString()); oAuth2Client.setClientAuthenticationMethod(UUID.randomUUID().toString()); oAuth2Client.setLoginButtonLabel(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonIcon(UUID.randomUUID().toString()); + oAuth2Client.setAdditionalInfo(JacksonUtil.newObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())); oAuth2Client.setMapperConfig( - OAuth2MapperConfig.builder() - .type(MapperType.CUSTOM) - .custom( - OAuth2CustomMapperConfig.builder() - .url(UUID.randomUUID().toString()) - .build() - ) - .build()); + OAuth2MapperConfig.builder() + .allowUserCreation(true) + .activateUser(true) + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build()); return oAuth2Client; } + private Domain constructDomain() { + Domain domain = new Domain(); + domain.setTenantId(TenantId.SYS_TENANT_ID); + domain.setName("my.edge.domain"); + domain.setOauth2Enabled(true); + domain.setPropagateToEdge(true); + return domain; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index d470ee686f..45dfed1bcb 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -47,7 +47,8 @@ import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; @@ -318,9 +319,14 @@ public class EdgeImitator { result.add(saveDownlinkMsg(resourceUpdateMsg)); } } - if (downlinkMsg.getOAuth2UpdateMsgCount() > 0) { - for (OAuth2UpdateMsg oAuth2UpdateMsg : downlinkMsg.getOAuth2UpdateMsgList()) { - result.add(saveDownlinkMsg(oAuth2UpdateMsg)); + if (downlinkMsg.getOAuth2ClientUpdateMsgCount() > 0) { + for (OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg : downlinkMsg.getOAuth2ClientUpdateMsgList()) { + result.add(saveDownlinkMsg(oAuth2ClientUpdateMsg)); + } + } + if (downlinkMsg.getOAuth2DomainUpdateMsgCount() > 0) { + for (OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg : downlinkMsg.getOAuth2DomainUpdateMsgList()) { + result.add(saveDownlinkMsg(oAuth2DomainUpdateMsg)); } } if (downlinkMsg.getNotificationTemplateUpdateMsgCount() > 0) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java index e4c67108db..317221fe0a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java @@ -49,4 +49,7 @@ public interface OAuth2ClientService extends EntityDaoService { PageData findOAuth2ClientInfosByTenantId(TenantId tenantId, PageLink pageLink); List findOAuth2ClientInfosByIds(TenantId tenantId, List oAuth2ClientIds); + + boolean isPropagateOAuth2ClientToEdge(TenantId tenantId, OAuth2ClientId oAuth2ClientId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 141966c446..ce26253f5a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -45,7 +45,8 @@ public enum EdgeEventType { NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), TB_RESOURCE(true, EntityType.TB_RESOURCE), - OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT); + OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), + DOMAIN(true, EntityType.DOMAIN); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index cbdaae3681..5a85e6ce67 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -159,6 +159,10 @@ public class EntityIdFactory { return new NotificationTargetId(uuid); case NOTIFICATION_TEMPLATE: return new NotificationTemplateId(uuid); + case OAUTH2_CLIENT: + return new OAuth2ClientId(uuid); + case DOMAIN: + return new DomainId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 0a36b5f56e..bfc95ea37c 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_7_0) + .setEdgeVersion(EdgeVersion.V_3_7_1) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 7c60985683..43ce1a222c 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -39,6 +39,7 @@ enum EdgeVersion { V_3_6_2 = 5; V_3_6_4 = 6; V_3_7_0 = 7; + V_3_7_1 = 8; } /** @@ -469,8 +470,18 @@ message ResourceUpdateMsg { string entity = 11; } -message OAuth2UpdateMsg { - string entity = 1; +message OAuth2ClientUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; +} + +message OAuth2DomainUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; } message NotificationRuleUpdateMsg { @@ -695,9 +706,9 @@ message DownlinkMsg { repeated TenantProfileUpdateMsg tenantProfileUpdateMsg = 27; repeated ResourceUpdateMsg resourceUpdateMsg = 28; repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 29; - repeated OAuth2UpdateMsg oAuth2UpdateMsg = 30; + repeated OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = 30; repeated NotificationRuleUpdateMsg notificationRuleUpdateMsg = 31; repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; + repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; } - diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java index b3380ef540..b7a13313b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java @@ -58,7 +58,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe log.trace("Executing saveDomain [{}]", domain); try { Domain savedDomain = domainDao.save(tenantId, domain); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedDomain).build()); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build()); return savedDomain; } catch (Exception e) { checkConstraintViolation(e, @@ -86,8 +86,6 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe for (DomainOauth2Client client : newClientList) { domainDao.addOauth2Client(client); } - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) - .entityId(domainId).created(false).build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java index e4bfcce4c5..cf40a331fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java @@ -43,4 +43,7 @@ public interface OAuth2ClientDao extends Dao { void deleteByTenantId(UUID tenantId); List findByIds(UUID tenantId, List oAuth2ClientIds); + + boolean isPropagateToEdge(TenantId tenantId, UUID oAuth2ClientId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index d0af6b22d0..6ab4909421 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -73,7 +73,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA log.trace("Executing saveOAuth2Client [{}]", oAuth2Client); oAuth2ClientDataValidator.validate(oAuth2Client, OAuth2Client::getTenantId); OAuth2Client savedOauth2Client = oauth2ClientDao.save(tenantId, oAuth2Client); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entity(oAuth2Client).build()); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedOauth2Client.getId()).entity(savedOauth2Client).build()); return savedOauth2Client; } @@ -129,6 +129,12 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA .collect(Collectors.toList()); } + @Override + public boolean isPropagateOAuth2ClientToEdge(TenantId tenantId, OAuth2ClientId oAuth2ClientId) { + log.trace("Executing isPropagateOAuth2ClientToEdge, tenantId [{}], oAuth2ClientId [{}]", tenantId, oAuth2ClientId); + return oauth2ClientDao.isPropagateToEdge(tenantId, oAuth2ClientId.getId()); + } + @Override public void deleteByTenantId(TenantId tenantId) { deleteOauth2ClientsByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java index 2cbe109af5..c2077bc2a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java @@ -67,7 +67,7 @@ public class JpaDomainDao extends JpaAbstractDao implement @Override public List findOauth2ClientsByDomainId(TenantId tenantId, DomainId domainId) { - return DaoUtil.convertDataList(domainOauth2ClientRepository.findAllByDomainId(domainId.getId())); + return DaoUtil.convertDataList(domainOauth2ClientRepository.findAllByDomainId(domainId.getId())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java index c8f16cf1c5..4a585186d9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java @@ -94,6 +94,11 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao findByTenantIdAndIdIn(UUID tenantId, List uuids); + @Query("SELECT COUNT(d) > 0 FROM DomainEntity d " + + "JOIN DomainOauth2ClientEntity doc ON d.id = doc.domainId " + + "WHERE d.tenantId = :tenantId AND doc.oauth2ClientId = :oAuth2ClientId AND d.propagateToEdge = true") + boolean isPropagateToEdge(@Param("tenantId") UUID tenantId, @Param("oAuth2ClientId") UUID oAuth2ClientId); + } From 5e47e1691305d9df3f5c4aef50d7551e6d09ce3f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 26 Aug 2024 16:12:27 +0300 Subject: [PATCH 289/331] Fix unknown security name --- .../snmp/service/SnmpAuthService.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java index bcc3240570..13f50621ad 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpAuthService.java @@ -24,6 +24,7 @@ import org.snmp4j.security.SecurityLevel; import org.snmp4j.security.SecurityModel; import org.snmp4j.security.SecurityProtocols; import org.snmp4j.security.USM; +import org.snmp4j.security.UsmUser; import org.snmp4j.smi.Address; import org.snmp4j.smi.GenericAddress; import org.snmp4j.smi.OID; @@ -72,28 +73,23 @@ public class SnmpAuthService { OctetString engineId = OctetString.fromString(deviceTransportConfig.getEngineId(), 16); OID authenticationProtocol = new OID(deviceTransportConfig.getAuthenticationProtocol().getOid()); - byte[] authenticationPassphrase = SecurityProtocols.getInstance().passwordToKey(authenticationProtocol, - new OctetString(deviceTransportConfig.getAuthenticationPassphrase()), engineId.getValue()); - if (authenticationPassphrase == null) { - throw new UnsupportedOperationException("Authentication protocol " + deviceTransportConfig.getAuthenticationProtocol() + " is not supported"); - } + OctetString authenticationPassphrase = Optional.ofNullable(SecurityProtocols.getInstance().passwordToKey(authenticationProtocol, + new OctetString(deviceTransportConfig.getAuthenticationPassphrase()), engineId.getValue())) + .map(OctetString::new) + .orElseThrow(() -> new UnsupportedOperationException("Authentication protocol " + deviceTransportConfig.getAuthenticationProtocol() + " is not supported")); OID privacyProtocol = new OID(deviceTransportConfig.getPrivacyProtocol().getOid()); - byte[] privacyPassphrase = SecurityProtocols.getInstance().passwordToKey(privacyProtocol, - authenticationProtocol, new OctetString(deviceTransportConfig.getPrivacyPassphrase()), engineId.getValue()); - if (privacyPassphrase == null) { - throw new UnsupportedOperationException("Privacy protocol " + deviceTransportConfig.getPrivacyProtocol() + " is not supported"); - } + OctetString privacyPassphrase = Optional.ofNullable(SecurityProtocols.getInstance().passwordToKey(privacyProtocol, + authenticationProtocol, new OctetString(deviceTransportConfig.getPrivacyPassphrase()), engineId.getValue())) + .map(OctetString::new) + .orElseThrow(() -> new UnsupportedOperationException("Privacy protocol " + deviceTransportConfig.getPrivacyProtocol() + " is not supported")); USM usm = snmpTransportService.getSnmp().getUSM(); if (usm.hasUser(engineId, securityName)) { usm.removeAllUsers(username, engineId); } - usm.addLocalizedUser( - engineId.getValue(), username, - authenticationProtocol, authenticationPassphrase, - privacyProtocol, privacyPassphrase - ); + UsmUser usmUser = new UsmUser(username, authenticationProtocol, authenticationPassphrase, privacyProtocol, privacyPassphrase, engineId); + usm.addUser(username, engineId, usmUser); UserTarget userTarget = new UserTarget(); userTarget.setSecurityName(securityName); From 73a45427d47232234419fa328159b3774a6001e9 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 27 Aug 2024 10:51:54 +0300 Subject: [PATCH 290/331] Tmp ignore testCreateUpdateDeleteTenantUser --- .../src/test/java/org/thingsboard/server/edge/UserEdgeTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 93fea90c5a..fc9eeaa36e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -45,6 +46,7 @@ public class UserEdgeTest extends AbstractEdgeTest { private BCryptPasswordEncoder passwordEncoder; @Test + @Ignore("Ignored until fix") public void testCreateUpdateDeleteTenantUser() throws Exception { // create user edgeImitator.expectMessageAmount(4); From c6ee0bff19f1ef7ee1320a140a43ae5d6543f897 Mon Sep 17 00:00:00 2001 From: Andrii Landiak <50847617+AndriiLandiak@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:41:39 +0300 Subject: [PATCH 291/331] Add edgeIdsCache to decrease number of calls to db for findRelatedEdgeByEntityId --- .../controller/plugin/TbWebSocketHandler.java | 2 +- .../edge/EdgeEventSourcingListener.java | 5 +- .../edge/RelatedEdgesSourcingListener.java | 80 +++ .../edge/rpc/processor/BaseEdgeProcessor.java | 8 +- .../processor/alarm/AlarmEdgeProcessor.java | 4 +- .../processor/asset/AssetEdgeProcessor.java | 16 +- .../dashboard/DashboardEdgeProcessor.java | 16 +- .../processor/device/DeviceEdgeProcessor.java | 10 +- .../rpc/processor/edge/EdgeProcessor.java | 2 +- .../entityview/EntityViewEdgeProcessor.java | 16 +- .../rule/RuleChainEdgeProcessor.java | 12 +- .../entitiy/EntityStateSourcingListener.java | 5 +- .../PubSubRuleNodeExecutorProvider.java | 4 +- .../HousekeeperReprocessingService.java | 2 +- .../housekeeper/HousekeeperService.java | 2 +- .../DefaultNotificationSchedulerService.java | 6 +- .../SubscriptionSchedulerComponent.java | 4 +- .../transport/DefaultTransportApiService.java | 4 +- .../src/main/resources/thingsboard.yml | 3 + ...CaffeineCacheDefaultConfigurationTest.java | 4 +- .../server/edge/RelationEdgeTest.java | 2 - .../RuleChainMsgConstructorTest.java | 414 ------------- .../rpc/processor/BaseEdgeProcessorTest.java | 563 ------------------ .../asset/AbstractAssetProcessorTest.java | 92 --- .../asset/AssetEdgeProcessorTest.java | 39 -- .../asset/AssetProfileEdgeProcessorTest.java | 40 -- .../device/AbstractDeviceProcessorTest.java | 102 ---- .../device/DeviceEdgeProcessorTest.java | 38 -- .../DeviceProfileEdgeProcessorTest.java | 41 -- .../telemetry/TelemetryEdgeProcessorTest.java | 66 -- .../DefaultTbRuleEngineRpcServiceTest.java | 1 + .../TbJsonTypeReferenceRedisSerializer.java | 40 ++ .../server/cache/device/DeviceCacheKey.java | 5 + .../cache}/edge/EdgeCacheEvictEvent.java | 4 +- .../server/cache}/edge/EdgeCacheKey.java | 6 +- .../server/cache}/edge/EdgeCaffeineCache.java | 2 +- .../server/cache}/edge/EdgeRedisCache.java | 3 +- .../cache/edge/RelatedEdgesCacheKey.java | 45 ++ .../cache/edge/RelatedEdgesCacheValue.java | 37 ++ .../cache/edge/RelatedEdgesCaffeineCache.java | 32 + .../cache/edge/RelatedEdgesEvictEvent.java | 30 + .../cache/edge/RelatedEdgesRedisCache.java | 35 ++ .../resourceInfo/ResourceInfoCacheKey.java | 5 + .../server/cluster/TbClusterService.java | 2 +- .../server/dao/edge/EdgeService.java | 4 + .../server/dao/edge/RelatedEdgesService.java | 30 + .../server/common/data/CacheConstants.java | 1 + .../server/common/data/DataConstants.java | 2 +- .../common/data/kv/BaseAttributeKvEntry.java | 1 + .../server/common/data/kv/BasicTsKvEntry.java | 1 - .../PageDataIterableByTenantIdEntityId.java | 2 +- .../server/queue/kafka/TbKafkaSettings.java | 2 +- .../discovery/ZkDiscoveryServiceTest.java | 1 - .../api/AbstractScriptInvokeService.java | 1 - .../script/api/ScriptStatCallback.java | 2 +- .../script/api/js/NashornJsInvokeService.java | 4 +- .../api/tbel/DefaultTbelInvokeService.java | 5 +- .../server/dao/asset/AssetCacheKey.java | 2 + .../dao/asset/AssetProfileCacheKey.java | 3 + .../dao/attributes/AttributeCacheKey.java | 4 + .../dao/attributes/BaseAttributesService.java | 1 - .../dao/dashboard/DashboardServiceImpl.java | 10 +- .../dao/device/DeviceProfileCacheKey.java | 3 + .../server/dao/device/DeviceServiceImpl.java | 8 +- .../dao/edge/BaseRelatedEdgesService.java | 62 ++ .../thingsboard/server/dao/edge/EdgeDao.java | 112 +--- .../server/dao/edge/EdgeServiceImpl.java | 38 +- .../dao/entity/EntityCountCacheKey.java | 2 + .../dao/entityview/EntityViewCacheKey.java | 4 + .../dao/entityview/EntityViewServiceImpl.java | 6 +- .../server/dao/ota/OtaPackageCacheKey.java | 4 + .../server/dao/relation/RelationCacheKey.java | 2 + .../server/dao/relation/RelationDao.java | 2 +- .../server/dao/sql/edge/EdgeRepository.java | 11 + .../server/dao/sql/edge/JpaEdgeDao.java | 13 + .../dao/tenant/TenantProfileCacheKey.java | 3 + .../dao/timeseries/TsLatestCacheKey.java | 4 +- .../dao/service/AbstractServiceTest.java | 4 +- .../server/dao/service/EntityServiceTest.java | 10 - .../dao/service/QueueStatsServiceTest.java | 2 - .../resources/application-test.properties | 3 + .../rule/engine/edge/TbMsgPushToEdgeNode.java | 6 +- .../rule/engine/aws/sns/TbSnsNodeTest.java | 2 +- .../rule/engine/aws/sqs/TbSqsNodeTest.java | 2 +- .../credentials/CertPemCredentialsTest.java | 2 +- .../engine/edge/TbMsgPushToEdgeNodeTest.java | 5 +- .../metadata/TbGetAttributesNodeTest.java | 1 + .../TbGetCustomerAttributeNodeTest.java | 1 - .../TbGetRelatedAttributeNodeTest.java | 1 - .../metadata/TbGetTelemetryNodeTest.java | 2 +- .../TbGetTenantAttributeNodeTest.java | 1 - .../telemetry/TbMsgAttributesNodeTest.java | 1 - 92 files changed, 592 insertions(+), 1650 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/edge/EdgeCacheEvictEvent.java (91%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/edge/EdgeCacheKey.java (87%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/edge/EdgeCaffeineCache.java (96%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/edge/EdgeRedisCache.java (97%) create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheKey.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCaffeineCache.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesEvictEvent.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesRedisCache.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/edge/RelatedEdgesService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index c37381c585..7fbe040091 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller.plugin; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; +import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.websocket.RemoteEndpoint; import jakarta.websocket.SendHandler; @@ -65,7 +66,6 @@ import org.thingsboard.server.service.ws.WsCommandsWrapper; import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper; -import javax.annotation.PostConstruct; import java.io.IOException; import java.security.InvalidParameterException; import java.util.Optional; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index f36f9ee2c3..789320e352 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -49,8 +50,6 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserServiceImpl; -import javax.annotation.PostConstruct; - /** * This event listener does not support async event processing because relay on ThreadLocal * Another possible approach is to implement a special annotation and a bunch of classes similar to TransactionalApplicationListener @@ -76,7 +75,7 @@ public class EdgeEventSourcingListener { @PostConstruct public void init() { - log.info("EdgeEventSourcingListener initiated"); + log.debug("EdgeEventSourcingListener initiated"); } @TransactionalEventListener(fallbackExecution = true) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java new file mode 100644 index 0000000000..851f59c3ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -0,0 +1,80 @@ +/** + * Copyright ยฉ 2016-2024 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.service.edge; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.dao.edge.RelatedEdgesService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RelatedEdgesSourcingListener { + + private final RelatedEdgesService relatedEdgesService; + + private ExecutorService executorService; + + @PostConstruct + public void init() { + log.debug("RelatedEdgesSourcingListener initiated"); + executorService = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("related-edges-listener")); + } + + @PreDestroy + public void destroy() { + log.debug("RelatedEdgesSourcingListener destroy"); + if (executorService != null && !executorService.isShutdown()) { + executorService.shutdown(); + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(ActionEntityEvent event) { + executorService.submit(() -> { + try { + switch (event.getActionType()) { + case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> + relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + } + } catch (Exception e) { + log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + } + }); + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(DeleteEntityEvent event) { + executorService.submit(() -> { + try { + relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + } catch (Exception e) { + log.error("[{}] failed to process DeleteEntityEvent: {}", event.getTenantId(), event, e); + } + }); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index bb1d6383e1..8dee74aac1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -133,14 +133,14 @@ import java.util.UUID; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; + @Slf4j public abstract class BaseEdgeProcessor { protected static final Lock deviceCreationLock = new ReentrantLock(); protected static final Lock assetCreationLock = new ReentrantLock(); - protected static final int DEFAULT_PAGE_SIZE = 100; - @Autowired protected TelemetrySubscriptionService tsSubService; @@ -507,7 +507,7 @@ public abstract class BaseEdgeProcessor { EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); + new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); @@ -653,7 +653,7 @@ public abstract class BaseEdgeProcessor { private boolean isEntityNotAssignedToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) { PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); + new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId edgeId1 : edgeIds) { if (edgeId1.equals(edgeId)) { return false; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index 22d76dc86e..009c5854cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -43,6 +43,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; + @Slf4j public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements AlarmProcessor { @@ -148,7 +150,7 @@ public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements A EdgeEventType edgeEventType) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, originatorId, DEFAULT_PAGE_SIZE); + new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, originatorId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, edgeEventType, actionType, alarmId, body)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 5d264cd83d..3e3c9dfb64 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -107,6 +107,7 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); @@ -120,20 +121,15 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion); - builder.addAssetProfileUpdateMsg(((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructAssetProfileUpdatedMsg(msgType, assetProfile)); + builder.addAssetProfileUpdateMsg(msgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile)); } downlinkMsg = builder.build(); } } - case DELETED, UNASSIGNED_FROM_EDGE -> { - AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor) - assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetDeleteMsg(assetId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addAssetUpdateMsg(assetUpdateMsg) - .build(); - } + case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetUpdateMsg(msgConstructor.constructAssetDeleteMsg(assetId)) + .build(); } return downlinkMsg; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index aab35f89d8..f5f25b1c56 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -100,27 +100,23 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (DashboardMsgConstructor) dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId); if (dashboard != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor) - dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardUpdatedMsg(msgType, dashboard); + DashboardUpdateMsg dashboardUpdateMsg = msgConstructor.constructDashboardUpdatedMsg(msgType, dashboard); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDashboardUpdateMsg(dashboardUpdateMsg) .build(); } } - case DELETED, UNASSIGNED_FROM_EDGE -> { - DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor) - dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardDeleteMsg(dashboardId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDashboardUpdateMsg(dashboardUpdateMsg) - .build(); - } + case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDashboardUpdateMsg(msgConstructor.constructDashboardDeleteMsg(dashboardId)) + .build(); } return downlinkMsg; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index a55dd77e01..52f09971ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -226,9 +226,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements Device device = deviceService.findDeviceById(edgeEvent.getTenantId(), deviceId); if (device != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructDeviceUpdatedMsg(msgType, device); + DeviceUpdateMsg deviceUpdateMsg = msgConstructor.constructDeviceUpdatedMsg(msgType, device); DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg); @@ -247,19 +245,17 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements break; case DELETED: case UNASSIGNED_FROM_EDGE: - DeviceUpdateMsg deviceUpdateMsg = msgConstructor.constructDeviceDeleteMsg(deviceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceUpdateMsg(deviceUpdateMsg) + .addDeviceUpdateMsg(msgConstructor.constructDeviceDeleteMsg(deviceId)) .build(); break; case CREDENTIALS_UPDATED: DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg) + .addDeviceCredentialsUpdateMsg(msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials)) .build(); } break; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java index 7bb631b330..f49c4b0a00 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java @@ -79,7 +79,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { List> futures = new ArrayList<>(); futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null)); futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.EDGE, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, edgeId, null)); - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageLink pageLink = new PageLink(1000); PageData pageData; do { pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index 70d9d9686f..4ef37f703b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -104,27 +104,23 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (EntityViewMsgConstructor) entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId); if (entityView != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor) - entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewUpdatedMsg(msgType, entityView); + EntityViewUpdateMsg entityViewUpdateMsg = msgConstructor.constructEntityViewUpdatedMsg(msgType, entityView); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addEntityViewUpdateMsg(entityViewUpdateMsg) .build(); } } - case DELETED, UNASSIGNED_FROM_EDGE -> { - EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor) - entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewDeleteMsg(entityViewId); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addEntityViewUpdateMsg(entityViewUpdateMsg) - .build(); - } + case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addEntityViewUpdateMsg(msgConstructor.constructEntityViewDeleteMsg(entityViewId)) + .build(); } return downlinkMsg; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index f63aa8b829..b71493fa39 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -42,6 +42,7 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE -> { RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); @@ -58,9 +59,7 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { isRoot = edge.getRootRuleChainId().equals(ruleChainId); } UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) - ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); + RuleChainUpdateMsg ruleChainUpdateMsg = msgConstructor.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) @@ -78,8 +77,7 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { } case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructRuleChainDeleteMsg(ruleChainId)) + .addRuleChainUpdateMsg(msgConstructor.constructRuleChainDeleteMsg(ruleChainId)) .build(); } return downlinkMsg; @@ -89,11 +87,11 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); DownlinkMsg downlinkMsg = null; + var msgConstructor = (RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); if (ruleChain != null) { RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor) - ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = msgConstructor .constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); if (ruleChainMetadataUpdateMsg != null) { downlinkMsg = DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 4a7c1e1e87..08b1e57651 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.entitiy; import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -53,7 +54,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; -import javax.annotation.PostConstruct; import java.util.Set; @Component @@ -66,7 +66,7 @@ public class EntityStateSourcingListener { @PostConstruct public void init() { - log.info("EntityStateSourcingListener initiated"); + log.debug("EntityStateSourcingListener initiated"); } @TransactionalEventListener(fallbackExecution = true) @@ -264,4 +264,5 @@ public class EntityStateSourcingListener { metaData.putValue("assignedFromTenantName", tenant.getName()); return metaData; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java b/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java index 8bfe8cf8a5..c8e30ba471 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.executors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -22,8 +24,6 @@ import org.thingsboard.common.util.ExecutorProvider; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.queue.util.TbRuleEngineComponent; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index 37a55c99d6..f0a6a58849 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.housekeeper; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.context.annotation.Lazy; @@ -33,7 +34,6 @@ import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; -import javax.annotation.PreDestroy; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index eea92eb75b..933f6fad3e 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.housekeeper; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -35,7 +36,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; -import javax.annotation.PreDestroy; import java.util.List; import java.util.Map; import java.util.Optional; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java index 5ba86198db..98e85077a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification; import com.google.common.util.concurrent.ListenableFuture; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,7 +42,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.partition.AbstractPartitionBasedService; -import javax.annotation.PreDestroy; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -75,9 +75,7 @@ public class DefaultNotificationSchedulerService extends AbstractPartitionBasedS @Override protected Map>> onAddedPartitions(Set addedPartitions) { - PageDataIterable notificationRequests = new PageDataIterable<>(pageLink -> { - return notificationRequestService.findScheduledNotificationRequests(pageLink); - }, 1000); + PageDataIterable notificationRequests = new PageDataIterable<>(notificationRequestService::findScheduledNotificationRequests, 1000); for (NotificationRequest notificationRequest : notificationRequests) { TopicPartitionInfo requestPartition = partitionService.resolve(ServiceType.TB_CORE, notificationRequest.getTenantId(), notificationRequest.getId()); if (addedPartitions.contains(requestPartition)) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionSchedulerComponent.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionSchedulerComponent.java index 7dcfee932f..680be6929d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionSchedulerComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionSchedulerComponent.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.service.subscription; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.queue.util.TbCoreComponent; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 8b29af34ed..b7c43391be 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -22,6 +22,8 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -102,8 +104,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentMap; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a0a8ea0c2f..c3a5998b60 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -572,6 +572,9 @@ cache: edges: timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}" # Edges cache TTL maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}" # 0 means the cache is disabled + relatedEdges: + timeToLiveInMinutes: "${CACHE_SPECS_RELATED_EDGES_TTL:1440}" # Related Edges cache TTL + maxSize: "${CACHE_SPECS_RELATED_EDGES_MAX_SIZE:10000}" # 0 means the cache is disabled repositorySettings: timeToLiveInMinutes: "${CACHE_SPECS_REPOSITORY_SETTINGS_TTL:1440}" # Repository settings cache TTL maxSize: "${CACHE_SPECS_REPOSITORY_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled diff --git a/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java b/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java index dac269d16a..d92ccfeacb 100644 --- a/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java +++ b/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java @@ -41,10 +41,10 @@ public class CaffeineCacheDefaultConfigurationTest { @Test public void verifyTransactionAwareCacheManagerProxy() { assertThat(cacheSpecsMap.getSpecs()).as("specs").isNotNull(); - cacheSpecsMap.getSpecs().forEach((name, cacheSpecs)->assertThat(cacheSpecs).as("cache %s specs", name).isNotNull()); + cacheSpecsMap.getSpecs().forEach((name, cacheSpecs) -> assertThat(cacheSpecs).as("cache %s specs", name).isNotNull()); SoftAssertions softly = new SoftAssertions(); - cacheSpecsMap.getSpecs().forEach((name, cacheSpecs)->{ + cacheSpecsMap.getSpecs().forEach((name, cacheSpecs) -> { softly.assertThat(name).as("cache name").isNotEmpty(); softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0); softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0); diff --git a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java index 82b7a09623..68d4df8fc8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java @@ -31,8 +31,6 @@ import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @DaoSqlTest public class RelationEdgeTest extends AbstractEdgeTest { diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java deleted file mode 100644 index ec43753596..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java +++ /dev/null @@ -1,414 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.constructor; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.msg.TbNodeConnectionType; -import org.thingsboard.server.common.data.rule.NodeConnectionInfo; -import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleNodeProto; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorV1; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Slf4j -@ExtendWith(MockitoExtension.class) -public class RuleChainMsgConstructorTest { - - private static final String RPC_CONNECTION_TYPE = "RPC"; - - private RuleChainMsgConstructorV1 ruleChainMsgConstructorV1; - - private TenantId tenantId; - - @BeforeEach - public void setup() { - ruleChainMsgConstructorV1 = new RuleChainMsgConstructorV1(); - tenantId = new TenantId(UUID.randomUUID()); - } - - @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() { - RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); - RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( - ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( - tenantId, - UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, - ruleChainMetaData, - EdgeVersion.V_3_4_0); - - assetV_3_3_3_and_V_3_4_0(ruleChainMetadataUpdateMsg); - - assertCheckpointRuleNodeConfiguration( - ruleChainMetadataUpdateMsg.getNodesList(), - "{\"queueName\":\"HighPriority\"}"); - } - - @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() { - RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); - RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( - ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( - tenantId, - UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, - ruleChainMetaData, - EdgeVersion.V_3_3_3); - - assetV_3_3_3_and_V_3_4_0(ruleChainMetadataUpdateMsg); - - assertCheckpointRuleNodeConfiguration( - ruleChainMetadataUpdateMsg.getNodesList(), - "{\"queueName\":\"HighPriority\"}"); - } - - private void assetV_3_3_3_and_V_3_4_0(RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) { - Assertions.assertEquals(3, ruleChainMetadataUpdateMsg.getFirstNodeIndex(), "First rule node index incorrect!"); - Assertions.assertEquals(12, ruleChainMetadataUpdateMsg.getNodesCount(), "Nodes count incorrect!"); - Assertions.assertEquals(13, ruleChainMetadataUpdateMsg.getConnectionsCount(), "Connections count incorrect!"); - Assertions.assertEquals(0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(), "Rule chain connections count incorrect!"); - - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 11, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 7, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 4, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 8, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(9)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(10)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(11)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(10, 9, RPC_CONNECTION_TYPE), ruleChainMetadataUpdateMsg.getConnections(12)); - } - - @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() { - RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); - RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( - tenantId, - UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, - ruleChainMetaData, - EdgeVersion.V_3_3_0); - - Assertions.assertEquals(2, ruleChainMetadataUpdateMsg.getFirstNodeIndex(),"First rule node index incorrect!"); - Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getNodesCount(), "Nodes count incorrect!"); - Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getConnectionsCount(),"Connections count incorrect!"); - Assertions.assertEquals(1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(), "Rule chain connections count incorrect!"); - - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 9, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 6, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 3, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 4, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 7, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 8, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); - - RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); - Assertions.assertEquals(2, ruleChainConnection.getFromIndex(), "From index incorrect!"); - Assertions.assertEquals(TbNodeConnectionType.SUCCESS, ruleChainConnection.getType(), "Type index incorrect!"); - Assertions.assertEquals("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", - ruleChainConnection.getAdditionalInfo(),"Additional info incorrect!"); - Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdMSB() != 0,"Target rule chain id MSB incorrect!"); - Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdLSB() != 0,"Target rule chain id LSB incorrect!"); - - assertCheckpointRuleNodeConfiguration( - ruleChainMetadataUpdateMsg.getNodesList(), - "{\"queueName\":\"HighPriority\"}"); - } - - @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() { - // same rule chain metadata, but different order of rule nodes - RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); - RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( - tenantId, - UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, - ruleChainMetaData1, - EdgeVersion.V_3_3_0); - - Assertions.assertEquals(7, ruleChainMetadataUpdateMsg.getFirstNodeIndex(), "First rule node index incorrect!"); - Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getNodesCount(),"Nodes count incorrect!"); - Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getConnectionsCount(),"Connections count incorrect!"); - Assertions.assertEquals(1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(),"Rule chain connections count incorrect!"); - - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 3, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 6, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 2, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 1, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); - - RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); - Assertions.assertEquals(7, ruleChainConnection.getFromIndex(),"From index incorrect!"); - Assertions.assertEquals(TbNodeConnectionType.SUCCESS, ruleChainConnection.getType(), "Type index incorrect!"); - Assertions.assertEquals( "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", - ruleChainConnection.getAdditionalInfo(), "Additional info incorrect!"); - Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdMSB() != 0, "Target rule chain id MSB incorrect!"); - Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdLSB() != 0, "Target rule chain id LSB incorrect!"); - - assertCheckpointRuleNodeConfiguration( - ruleChainMetadataUpdateMsg.getNodesList(), - "{\"queueName\":\"HighPriority\"}"); - } - - private void assertCheckpointRuleNodeConfiguration(List nodesList, - String expectedConfiguration) { - Optional checkpointRuleNodeOpt = nodesList.stream() - .filter(rn -> "org.thingsboard.rule.engine.flow.TbCheckpointNode".equals(rn.getType())) - .findFirst(); - Assertions.assertTrue(checkpointRuleNodeOpt.isPresent()); - RuleNodeProto checkpointRuleNode = checkpointRuleNodeOpt.get(); - Assertions.assertEquals(expectedConfiguration, checkpointRuleNode.getConfiguration()); - } - - private void compareNodeConnectionInfoAndProto(NodeConnectionInfo expected, org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto actual) { - Assertions.assertEquals(expected.getFromIndex(), actual.getFromIndex()); - Assertions.assertEquals(expected.getToIndex(), actual.getToIndex()); - Assertions.assertEquals(expected.getType(), actual.getType()); - } - - private RuleChainMetaData createRuleChainMetaData(RuleChainId ruleChainId, Integer firstNodeIndex, List nodes, List connections) { - RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); - ruleChainMetaData.setRuleChainId(ruleChainId); - ruleChainMetaData.setFirstNodeIndex(firstNodeIndex); - ruleChainMetaData.setNodes(nodes); - ruleChainMetaData.setConnections(connections); - ruleChainMetaData.setRuleChainConnections(null); - return ruleChainMetaData; - } - - private List createConnections() { - List result = new ArrayList<>(); - result.add(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(4, 11, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(5, 11, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(6, 11, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(6, 7, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(6, 4, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(6, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(6, 8, TbNodeConnectionType.OTHER)); - result.add(createNodeConnectionInfo(6, 9, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(7, 11, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(10, 9, RPC_CONNECTION_TYPE)); - return result; - } - - private NodeConnectionInfo createNodeConnectionInfo(int fromIndex, int toIndex, String type) { - NodeConnectionInfo result = new NodeConnectionInfo(); - result.setFromIndex(fromIndex); - result.setToIndex(toIndex); - result.setType(type); - return result; - } - - private List createRuleNodes(RuleChainId ruleChainId) { - List result = new ArrayList<>(); - result.add(getOutputNode(ruleChainId)); - result.add(getAcknowledgeNode(ruleChainId)); - result.add(getCheckpointNode(ruleChainId)); - result.add(getDeviceProfileNode(ruleChainId)); - result.add(getSaveTimeSeriesNode(ruleChainId)); - result.add(getSaveClientAttributesNode(ruleChainId)); - result.add(getMessageTypeSwitchNode(ruleChainId)); - result.add(getLogRpcFromDeviceNode(ruleChainId)); - result.add(getLogOtherNode(ruleChainId)); - result.add(getRpcCallRequestNode(ruleChainId)); - result.add(getPushToAnalyticsNode(ruleChainId)); - result.add(getPushToCloudNode(ruleChainId)); - return result; - } - - private RuleNode createRuleNode(RuleChainId ruleChainId, String type, String name, JsonNode configuration, JsonNode additionalInfo) { - RuleNode e = new RuleNode(); - e.setRuleChainId(ruleChainId); - e.setType(type); - e.setName(name); - e.setDebugMode(false); - e.setConfiguration(configuration); - e.setAdditionalInfo(additionalInfo); - e.setId(new RuleNodeId(UUID.randomUUID())); - return e; - } - - private List createConnectionsInDifferentOrder() { - List result = new ArrayList<>(); - result.add(createNodeConnectionInfo(0, 2, RPC_CONNECTION_TYPE)); - result.add(createNodeConnectionInfo(4, 1, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(5, 1, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(5, 4, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(5, 7, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(5, 6, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(5, 3, TbNodeConnectionType.OTHER)); - result.add(createNodeConnectionInfo(5, 2, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection())); - result.add(createNodeConnectionInfo(6, 1, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(7, 1, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(8, 11, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(8, 5, TbNodeConnectionType.SUCCESS)); - result.add(createNodeConnectionInfo(8, 0, TbNodeConnectionType.SUCCESS)); - return result; - } - - private List createRuleNodesInDifferentOrder(RuleChainId ruleChainId) { - List result = new ArrayList<>(); - result.add(getPushToAnalyticsNode(ruleChainId)); - result.add(getPushToCloudNode(ruleChainId)); - result.add(getRpcCallRequestNode(ruleChainId)); - result.add(getLogOtherNode(ruleChainId)); - result.add(getLogRpcFromDeviceNode(ruleChainId)); - result.add(getMessageTypeSwitchNode(ruleChainId)); - result.add(getSaveClientAttributesNode(ruleChainId)); - result.add(getSaveTimeSeriesNode(ruleChainId)); - result.add(getDeviceProfileNode(ruleChainId)); - result.add(getCheckpointNode(ruleChainId)); - result.add(getAcknowledgeNode(ruleChainId)); - result.add(getOutputNode(ruleChainId)); - return result; - } - - - private RuleNode getOutputNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.flow.TbRuleChainOutputNode", - "Output node", - JacksonUtil.toJsonNode("{\"version\":0}"), - JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}")); - } - - private RuleNode getCheckpointNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.flow.TbCheckpointNode", - "Checkpoint node", - JacksonUtil.toJsonNode("{\"queueName\":\"HighPriority\"}"), - JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}")); - } - - private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", - "Save Timeseries", - JacksonUtil.toJsonNode("{\"defaultTTL\":0}"), - JacksonUtil.toJsonNode("{\"layoutX\":823,\"layoutY\":157}")); - } - - private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", - "Message Type Switch", - JacksonUtil.toJsonNode("{\"version\":0}"), - JacksonUtil.toJsonNode("{\"layoutX\":347,\"layoutY\":149}")); - } - - private RuleNode getLogOtherNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.action.TbLogNode", - "Log Other", - JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), - JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":378}")); - } - - private RuleNode getPushToCloudNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", - "Push to cloud", - JacksonUtil.toJsonNode("{\"scope\":\"SERVER_SCOPE\"}"), - JacksonUtil.toJsonNode("{\"layoutX\":1129,\"layoutY\":52}")); - } - - private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.flow.TbAckNode", - "Acknowledge node", - JacksonUtil.toJsonNode("{\"version\":0}"), - JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}")); - } - - private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "Device Profile Node", - JacksonUtil.toJsonNode("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"), - JacksonUtil.toJsonNode("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}")); - } - - private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", - "Save Client Attributes", - JacksonUtil.toJsonNode("{\"scope\":\"CLIENT_SCOPE\"}"), - JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":52}")); - } - - private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.action.TbLogNode", - "Log RPC from Device", - JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), - JacksonUtil.toJsonNode("{\"layoutX\":825,\"layoutY\":266}")); - } - - private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", - "RPC Call Request", - JacksonUtil.toJsonNode("{\"timeoutInSeconds\":60}"), - JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":466}")); - } - - private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) { - return createRuleNode(ruleChainId, - "org.thingsboard.rule.engine.flow.TbRuleChainInputNode", - "Push to Analytics", - JacksonUtil.toJsonNode("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"), - JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}")); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java deleted file mode 100644 index ac21141b75..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ /dev/null @@ -1,563 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor; - -import org.junit.jupiter.params.provider.Arguments; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.context.annotation.Lazy; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.TbResource; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.alarm.AlarmCommentService; -import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.device.DeviceCredentialsService; -import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.edge.EdgeEventService; -import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; -import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.notification.NotificationRuleService; -import org.thingsboard.server.dao.notification.NotificationTargetService; -import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.oauth2.OAuth2ClientService; -import org.thingsboard.server.dao.ota.OtaPackageService; -import org.thingsboard.server.dao.queue.QueueService; -import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.ImageService; -import org.thingsboard.server.dao.resource.ResourceService; -import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.tenant.TenantProfileService; -import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.notification.NotificationMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.oauth2.OAuth2MsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.telemetry.EntityDataMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorFactory; -import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorV1; -import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorV2; -import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorV2; -import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgeProcessorFactory; -import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgeProcessorV1; -import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgeProcessorV2; -import org.thingsboard.server.service.entitiy.TbLogEntityActionService; -import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.profile.TbAssetProfileCache; -import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.state.DeviceStateService; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; - -import java.util.UUID; -import java.util.stream.Stream; - -public abstract class BaseEdgeProcessorTest { - - @MockBean - protected TelemetrySubscriptionService tsSubService; - - @MockBean - protected TbLogEntityActionService logEntityActionService; - - @MockBean - protected RuleChainService ruleChainService; - - @MockBean - protected AlarmService alarmService; - - @MockBean - protected AlarmCommentService alarmCommentService; - - @MockBean - protected DeviceService deviceService; - - @MockBean - protected TbDeviceProfileCache deviceProfileCache; - - @MockBean - protected TbAssetProfileCache assetProfileCache; - - @MockBean - protected DashboardService dashboardService; - - @MockBean - protected AssetService assetService; - - @MockBean - protected EntityViewService entityViewService; - - @MockBean - protected TenantService tenantService; - - @MockBean - protected TenantProfileService tenantProfileService; - - @MockBean - protected EdgeService edgeService; - - @MockBean - protected CustomerService customerService; - - @MockBean - protected UserService userService; - - @MockBean - protected NotificationRuleService notificationRuleService; - - @MockBean - protected NotificationTargetService notificationTargetService; - - @MockBean - protected NotificationTemplateService notificationTemplateService; - - @MockBean - protected DeviceProfileService deviceProfileService; - - @MockBean - protected AssetProfileService assetProfileService; - - @MockBean - protected RelationService relationService; - - @MockBean - protected DeviceCredentialsService deviceCredentialsService; - - @MockBean - protected AttributesService attributesService; - - @MockBean - protected TimeseriesService timeseriesService; - - @MockBean - protected TbClusterService tbClusterService; - - @MockBean - protected DeviceStateService deviceStateService; - - @MockBean - protected EdgeEventService edgeEventService; - - @MockBean - protected WidgetsBundleService widgetsBundleService; - - @MockBean - protected WidgetTypeService widgetTypeService; - - @MockBean - protected OtaPackageService otaPackageService; - - @MockBean - protected QueueService queueService; - - @MockBean - protected PartitionService partitionService; - - @MockBean - protected ResourceService resourceService; - - @MockBean - protected OAuth2ClientService oAuth2ClientService; - - @MockBean - @Lazy - protected TbQueueProducerProvider producerProvider; - - @MockBean - protected DataValidator deviceValidator; - - @MockBean - protected DataValidator deviceProfileValidator; - - @MockBean - protected DataValidator assetValidator; - - @MockBean - protected DataValidator assetProfileValidator; - - @MockBean - protected DataValidator dashboardValidator; - - @MockBean - protected DataValidator entityViewValidator; - - @MockBean - protected DataValidator resourceValidator; - - @MockBean - protected EdgeMsgConstructor edgeMsgConstructor; - - @MockBean - protected EntityDataMsgConstructor entityDataMsgConstructor; - - @MockBean - protected AdminSettingsMsgConstructorV1 adminSettingsMsgConstructorV1; - - @MockBean - protected AdminSettingsMsgConstructorV2 adminSettingsMsgConstructorV2; - - @MockBean - protected AlarmMsgConstructorV1 alarmMsgConstructorV1; - - @MockBean - protected AlarmMsgConstructorV2 alarmMsgConstructorV2; - - @SpyBean - protected AssetMsgConstructorV1 assetMsgConstructorV1; - - @SpyBean - protected AssetMsgConstructorV2 assetMsgConstructorV2; - - @MockBean - protected CustomerMsgConstructorV1 customerMsgConstructorV1; - - @MockBean - protected CustomerMsgConstructorV2 customerMsgConstructorV2; - - @MockBean - protected DashboardMsgConstructorV1 dashboardMsgConstructorV1; - - @MockBean - protected DashboardMsgConstructorV2 dashboardMsgConstructorV2; - - @SpyBean - protected DeviceMsgConstructorV1 deviceMsgConstructorV1; - - @SpyBean - protected DeviceMsgConstructorV2 deviceMsgConstructorV2; - - @MockBean - protected EntityViewMsgConstructorV1 entityViewMsgConstructorV1; - - @MockBean - protected EntityViewMsgConstructorV2 entityViewMsgConstructorV2; - - @MockBean - protected OtaPackageMsgConstructorV1 otaPackageMsgConstructorV1; - - @MockBean - protected OtaPackageMsgConstructorV2 otaPackageMsgConstructorV2; - - @MockBean - protected QueueMsgConstructorV1 queueMsgConstructorV1; - - @MockBean - protected QueueMsgConstructorV2 queueMsgConstructorV2; - - @MockBean - protected RelationMsgConstructorV1 relationMsgConstructorV1; - - @MockBean - protected RelationMsgConstructorV2 relationMsgConstructorV2; - - @MockBean - protected ResourceMsgConstructorV1 resourceMsgConstructorV1; - - @MockBean - protected ResourceMsgConstructorV2 resourceMsgConstructorV2; - - @SpyBean - protected RuleChainMsgConstructorV1 ruleChainMsgConstructorV1; - - @SpyBean - protected RuleChainMsgConstructorV2 ruleChainMsgConstructorV2; - - @MockBean - protected TenantMsgConstructorV1 tenantMsgConstructorV1; - - @MockBean - protected TenantMsgConstructorV2 tenantMsgConstructorV2; - - @MockBean - protected UserMsgConstructorV1 userMsgConstructorV1; - - @MockBean - protected UserMsgConstructorV2 userMsgConstructorV2; - - @MockBean - protected WidgetMsgConstructorV1 widgetMsgConstructorV1; - - @MockBean - protected WidgetMsgConstructorV2 widgetMsgConstructorV2; - - @MockBean - protected NotificationMsgConstructor notificationMsgConstructor; - - @MockBean - protected OAuth2MsgConstructor oAuth2MsgConstructor; - - @MockBean - protected AlarmEdgeProcessorV1 alarmProcessorV1; - - @MockBean - protected AlarmEdgeProcessorV2 alarmProcessorV2; - - @SpyBean - protected AssetEdgeProcessorV1 assetProcessorV1; - - @SpyBean - protected AssetEdgeProcessorV2 assetProcessorV2; - - @SpyBean - protected AssetProfileEdgeProcessorV1 assetProfileProcessorV1; - - @SpyBean - protected AssetProfileEdgeProcessorV2 assetProfileProcessorV2; - - @MockBean - protected DashboardEdgeProcessorV1 dashboardProcessorV1; - - @MockBean - protected DashboardEdgeProcessorV2 dashboardProcessorV2; - - @MockBean - protected ImageService imageService; - - @SpyBean - protected DeviceEdgeProcessorV1 deviceEdgeProcessorV1; - - @SpyBean - protected DeviceEdgeProcessorV2 deviceEdgeProcessorV2; - - @SpyBean - protected DeviceProfileEdgeProcessorV1 deviceProfileProcessorV1; - - @SpyBean - protected DeviceProfileEdgeProcessorV2 deviceProfileProcessorV2; - - @MockBean - protected EntityViewProcessorV1 entityViewProcessorV1; - - @MockBean - protected EntityViewProcessorV2 entityViewProcessorV2; - - @MockBean - protected ResourceEdgeProcessorV1 resourceEdgeProcessorV1; - - @MockBean - protected ResourceEdgeProcessorV2 resourceEdgeProcessorV2; - - @MockBean - protected RelationEdgeProcessorV1 relationEdgeProcessorV1; - - @MockBean - protected RelationEdgeProcessorV2 relationEdgeProcessorV2; - - @MockBean - protected OAuth2EdgeProcessor oAuth2EdgeProcessor; - - @MockBean - protected NotificationEdgeProcessor notificationEdgeProcessor; - - @SpyBean - protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; - - @MockBean - protected AlarmMsgConstructorFactory alarmMsgConstructorFactory; - - @SpyBean - protected DeviceMsgConstructorFactory deviceMsgConstructorFactory; - - @SpyBean - protected AssetMsgConstructorFactory assetMsgConstructorFactory; - - @MockBean - protected DashboardMsgConstructorFactory dashboardMsgConstructorFactory; - - @MockBean - protected EntityViewMsgConstructorFactory entityViewMsgConstructorFactory; - - @MockBean - protected RelationMsgConstructorFactory relationMsgConstructorFactory; - - @MockBean - protected UserMsgConstructorFactory userMsgConstructorFactory; - - @MockBean - protected CustomerMsgConstructorFactory customerMsgConstructorFactory; - - @MockBean - protected TenantMsgConstructorFactory tenantMsgConstructorFactory; - - @MockBean - protected WidgetMsgConstructorFactory widgetBundleMsgConstructorFactory; - - @MockBean - protected AdminSettingsMsgConstructorFactory adminSettingsMsgConstructorFactory; - - @MockBean - protected OtaPackageMsgConstructorFactory otaPackageMsgConstructorFactory; - - @MockBean - protected QueueMsgConstructorFactory queueMsgConstructorFactory; - - @MockBean - protected ResourceMsgConstructorFactory resourceMsgConstructorFactory; - - @MockBean - protected AlarmEdgeProcessorFactory alarmEdgeProcessorFactory; - - @SpyBean - protected AssetEdgeProcessorFactory assetEdgeProcessorFactory; - - @MockBean - protected DashboardEdgeProcessorFactory dashboardEdgeProcessorFactory; - - @SpyBean - protected DeviceEdgeProcessorFactory deviceEdgeProcessorFactory; - - @MockBean - protected EntityViewProcessorFactory entityViewProcessorFactory; - - @MockBean - protected RelationEdgeProcessorFactory relationEdgeProcessorFactory; - - @MockBean - protected ResourceEdgeProcessorFactory resourceEdgeProcessorFactory; - - @MockBean - protected EdgeSynchronizationManager edgeSynchronizationManager; - - @MockBean - protected DbCallbackExecutorService dbCallbackExecutorService; - - protected EdgeId edgeId; - protected TenantId tenantId; - protected EdgeEvent edgeEvent; - - protected DashboardId getDashboardId(long expectedDashboardIdMSB, long expectedDashboardIdLSB) { - DashboardId dashboardId; - if (expectedDashboardIdMSB != 0 && expectedDashboardIdLSB != 0) { - dashboardId = new DashboardId(new UUID(expectedDashboardIdMSB, expectedDashboardIdLSB)); - } else { - dashboardId = new DashboardId(UUID.randomUUID()); - } - return dashboardId; - } - - protected RuleChainId getRuleChainId(long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - RuleChainId ruleChainId; - if (expectedRuleChainIdMSB != 0 && expectedRuleChainIdLSB != 0) { - ruleChainId = new RuleChainId(new UUID(expectedRuleChainIdMSB, expectedRuleChainIdLSB)); - } else { - ruleChainId = new RuleChainId(UUID.randomUUID()); - } - return ruleChainId; - } - - protected static Stream provideParameters() { - UUID dashboardUUID = UUID.randomUUID(); - UUID ruleChainUUID = UUID.randomUUID(); - return Stream.of( - Arguments.of(EdgeVersion.V_3_3_0, 0, 0, 0, 0), - Arguments.of(EdgeVersion.V_3_3_3, 0, 0, 0, 0), - Arguments.of(EdgeVersion.V_3_4_0, 0, 0, 0, 0), - Arguments.of(EdgeVersion.V_3_6_0, - dashboardUUID.getMostSignificantBits(), - dashboardUUID.getLeastSignificantBits(), - ruleChainUUID.getMostSignificantBits(), - ruleChainUUID.getLeastSignificantBits()) - ); - } - -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java deleted file mode 100644 index d87cc776cd..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.asset; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.BDDMockito.willReturn; - -public abstract class AbstractAssetProcessorTest extends BaseEdgeProcessorTest { - - - protected AssetId assetId; - protected AssetProfileId assetProfileId; - protected AssetProfile assetProfile; - - @BeforeEach - public void setUp() { - edgeId = new EdgeId(UUID.randomUUID()); - tenantId = new TenantId(UUID.randomUUID()); - assetId = new AssetId(UUID.randomUUID()); - assetProfileId = new AssetProfileId(UUID.randomUUID()); - - assetProfile = new AssetProfile(); - assetProfile.setId(assetProfileId); - assetProfile.setName("AssetProfile"); - assetProfile.setDefault(true); - - Asset asset = new Asset(); - asset.setAssetProfileId(assetProfileId); - asset.setId(assetId); - asset.setName("Asset"); - asset.setType(assetProfile.getName()); - - edgeEvent = new EdgeEvent(); - edgeEvent.setTenantId(tenantId); - edgeEvent.setAction(EdgeEventActionType.ADDED); - - - willReturn(asset).given(assetService).findAssetById(tenantId, assetId); - willReturn(assetProfile).given(assetProfileService).findAssetProfileById(tenantId, assetProfileId); - } - - protected void updateAssetProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB); - RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB); - - assetProfile.setDefaultDashboardId(dashboardId); - assetProfile.setDefaultEdgeRuleChainId(ruleChainId); - - } - - protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - AssetProfileUpdateMsg assetProfileUpdateMsg = downlinkMsg.getAssetProfileUpdateMsgList().get(0); - assertNotNull(assetProfileUpdateMsg); - Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB); - Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB); - Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB); - Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java deleted file mode 100644 index dab779c326..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.asset; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.test.context.SpringBootTest; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; - -@SpringBootTest(classes = {AssetEdgeProcessorV1.class}) -class AssetEdgeProcessorTest extends AbstractAssetProcessorTest { - - @ParameterizedTest - @MethodSource("provideParameters") - public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - - edgeEvent.setEntityId(assetId.getId()); - - DownlinkMsg downlinkMsg = assetProcessorV1.convertAssetEventToDownlink(edgeEvent, edgeId, edgeVersion); - - verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java deleted file mode 100644 index edbab05ccd..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.asset; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.test.context.SpringBootTest; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; - -@SpringBootTest(classes = {AssetEdgeProcessorV1.class}) -class AssetProfileEdgeProcessorTest extends AbstractAssetProcessorTest{ - - @ParameterizedTest - @MethodSource("provideParameters") - public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - - updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - - edgeEvent.setEntityId(assetProfileId.getId()); - - DownlinkMsg downlinkMsg = assetProfileProcessorV1.convertAssetProfileEventToDownlink(edgeEvent, edgeId, edgeVersion); - - verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java deleted file mode 100644 index 31524e467a..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.device; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceProfileType; -import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.device.profile.DeviceProfileData; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.BDDMockito.willReturn; - -public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest { - - protected DeviceId deviceId; - protected DeviceProfileId deviceProfileId; - protected DeviceProfile deviceProfile; - - @BeforeEach - public void setUp() { - edgeId = new EdgeId(UUID.randomUUID()); - tenantId = new TenantId(UUID.randomUUID()); - deviceId = new DeviceId(UUID.randomUUID()); - deviceProfileId = new DeviceProfileId(UUID.randomUUID()); - - deviceProfile = new DeviceProfile(); - deviceProfile.setId(deviceProfileId); - deviceProfile.setName("DeviceProfile"); - deviceProfile.setDefault(true); - deviceProfile.setType(DeviceProfileType.DEFAULT); - DeviceProfileData deviceProfileData = new DeviceProfileData(); - deviceProfile.setProfileData(deviceProfileData); - deviceProfile.setTransportType(DeviceTransportType.DEFAULT); - - DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentials.setDeviceId(deviceId); - - Device device = new Device(); - device.setDeviceProfileId(deviceProfileId); - device.setId(deviceId); - device.setName("Device"); - device.setType(deviceProfile.getName()); - - edgeEvent = new EdgeEvent(); - edgeEvent.setTenantId(tenantId); - edgeEvent.setAction(EdgeEventActionType.ADDED); - - willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId); - willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId); - willReturn(deviceCredentials).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(tenantId, deviceId); - } - - protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB); - RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB); - - deviceProfile.setDefaultDashboardId(dashboardId); - deviceProfile.setDefaultEdgeRuleChainId(ruleChainId); - - } - - protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - DeviceProfileUpdateMsg deviceProfileUpdateMsg = downlinkMsg.getDeviceProfileUpdateMsgList().get(0); - assertNotNull(deviceProfileUpdateMsg); - Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB); - Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB); - Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB); - Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java deleted file mode 100644 index 238a79f9d2..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.device; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.test.context.SpringBootTest; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; - -@SpringBootTest(classes = {DeviceEdgeProcessorV1.class}) -class DeviceEdgeProcessorTest extends AbstractDeviceProcessorTest { - - @ParameterizedTest - @MethodSource("provideParameters") - public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - edgeEvent.setEntityId(deviceId.getId()); - - DownlinkMsg downlinkMsg = deviceEdgeProcessorV1.convertDeviceEventToDownlink(edgeEvent, edgeId, edgeVersion); - - verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java deleted file mode 100644 index 179f524f6d..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.device; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.test.context.SpringBootTest; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessorV1; - - -@SpringBootTest(classes = {DeviceProfileEdgeProcessorV1.class}) -class DeviceProfileEdgeProcessorTest extends AbstractDeviceProcessorTest { - - @ParameterizedTest - @MethodSource("provideParameters") - public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, - long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { - updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - - edgeEvent.setEntityId(deviceProfileId.getId()); - - DownlinkMsg downlinkMsg = deviceProfileProcessorV1.convertDeviceProfileEventToDownlink(edgeEvent, edgeId, edgeVersion); - - verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java deleted file mode 100644 index 94714af463..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge.rpc.processor.telemetry; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; - -@RunWith(SpringRunner.class) -@ContextConfiguration(classes = {TelemetryEdgeProcessor.class}) -@TestPropertySource(properties = { - "edges.rpc.max_telemetry_message_size=1000" -}) -public class TelemetryEdgeProcessorTest extends BaseEdgeProcessorTest { - - @SpyBean - private TelemetryEdgeProcessor telemetryEdgeProcessor; - - @MockBean - private NotificationRuleProcessor notificationRuleProcessor; - - @Test - public void testConvert_maxSizeLimit() { - Edge edge = new Edge(); - EdgeEvent edgeEvent = new EdgeEvent(); - ObjectNode body = JacksonUtil.newObjectNode(); - body.put("value", StringUtils.randomAlphanumeric(1000)); - edgeEvent.setBody(body); - - DownlinkMsg downlinkMsg = telemetryEdgeProcessor.convertTelemetryEventToDownlink(edge, edgeEvent); - Assert.assertNull(downlinkMsg); - - verify(notificationRuleProcessor, Mockito.times(1)).process(any()); - } - -} diff --git a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java index 0ed53b9561..2cab5f991a 100644 --- a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.gen.transport.TransportProtos; + import java.util.UUID; import static org.mockito.BDDMockito.then; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java new file mode 100644 index 0000000000..f56f1f70b2 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java @@ -0,0 +1,40 @@ +/** + * Copyright ยฉ 2016-2024 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.cache; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.springframework.data.redis.serializer.SerializationException; +import org.thingsboard.common.util.JacksonUtil; + +public class TbJsonTypeReferenceRedisSerializer implements TbRedisSerializer { + + private final TypeReference typeReference; + + public TbJsonTypeReferenceRedisSerializer(TypeReference typeReference) { + this.typeReference = typeReference; + } + + @Override + public byte[] serialize(V v) throws SerializationException { + return JacksonUtil.writeValueAsBytes(v); + } + + @Override + public V deserialize(K key, byte[] bytes) throws SerializationException { + return JacksonUtil.fromBytes(bytes, typeReference); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java index 895af47a09..ed6258842d 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java @@ -22,6 +22,7 @@ import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -30,6 +31,9 @@ import java.io.Serializable; @Builder public class DeviceCacheKey implements Serializable { + @Serial + private static final long serialVersionUID = 6366389552842340207L; + private final TenantId tenantId; private final DeviceId deviceId; private final String deviceName; @@ -56,4 +60,5 @@ public class DeviceCacheKey implements Serializable { return tenantId + "_" + deviceId; } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCacheEvictEvent.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java rename to common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCacheEvictEvent.java index 36487b2422..6d02cbfe5a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCacheEvictEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.edge; +package org.thingsboard.server.cache.edge; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.TenantId; @Data @RequiredArgsConstructor -class EdgeCacheEvictEvent { +public class EdgeCacheEvictEvent { private final TenantId tenantId; private final String newName; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCacheKey.java similarity index 87% rename from dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java rename to common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCacheKey.java index b07dfc32da..9ae0eaf54e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCacheKey.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.edge; +package org.thingsboard.server.cache.edge; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -29,6 +30,9 @@ import java.io.Serializable; @Builder public class EdgeCacheKey implements Serializable { + @Serial + private static final long serialVersionUID = -2299543993746815287L; + private final TenantId tenantId; private final String name; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCaffeineCache.java similarity index 96% rename from dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCaffeineCache.java index baba0dfd68..9204ba1848 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeCaffeineCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.edge; +package org.thingsboard.server.cache.edge; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeRedisCache.java similarity index 97% rename from dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeRedisCache.java index f1502e9ad7..7fa8876a69 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/EdgeRedisCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.edge; +package org.thingsboard.server.cache.edge; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -32,4 +32,5 @@ public class EdgeRedisCache extends RedisTbTransactionalCache(Edge.class)); } + } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheKey.java new file mode 100644 index 0000000000..7fbf24bb40 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheKey.java @@ -0,0 +1,45 @@ +/** + * Copyright ยฉ 2016-2024 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.cache.edge; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serial; +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class RelatedEdgesCacheKey implements Serializable { + + @Serial + private static final long serialVersionUID = 5118170671697650121L; + + private final TenantId tenantId; + private final EntityId entityId; + + @Override + public String toString() { + return tenantId + "_" + entityId; + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java new file mode 100644 index 0000000000..a46b2c5875 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java @@ -0,0 +1,37 @@ +/** + * Copyright ยฉ 2016-2024 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.cache.edge; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.page.PageData; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class RelatedEdgesCacheValue implements Serializable { + + @Serial + private static final long serialVersionUID = -2765080094748518572L; + + private final PageData pageData; + + public RelatedEdgesCacheValue(PageData pageData) { + this.pageData = pageData; + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCaffeineCache.java new file mode 100644 index 0000000000..e2007e9b09 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCaffeineCache.java @@ -0,0 +1,32 @@ +/** + * Copyright ยฉ 2016-2024 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.cache.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("RelatedEdgeIdsCache") +public class RelatedEdgesCaffeineCache extends CaffeineTbTransactionalCache { + + public RelatedEdgesCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.RELATED_EDGES_CACHE); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesEvictEvent.java new file mode 100644 index 0000000000..4b079668ec --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesEvictEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright ยฉ 2016-2024 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.cache.edge; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +public class RelatedEdgesEvictEvent { + + private final TenantId tenantId; + private final EntityId entityId; + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesRedisCache.java new file mode 100644 index 0000000000..099e3e65bc --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesRedisCache.java @@ -0,0 +1,35 @@ +/** + * Copyright ยฉ 2016-2024 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.cache.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("RelatedEdgeIdsCache") +public class RelatedEdgesRedisCache extends RedisTbTransactionalCache { + + public RelatedEdgesRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.RELATED_EDGES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(RelatedEdgesCacheValue.class)); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java index 2cf1768751..45de55057f 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java @@ -22,6 +22,7 @@ import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -30,6 +31,9 @@ import java.io.Serializable; @Builder public class ResourceInfoCacheKey implements Serializable { + @Serial + private static final long serialVersionUID = 2100510964692846992L; + private final TenantId tenantId; private final TbResourceId tbResourceId; @@ -37,4 +41,5 @@ public class ResourceInfoCacheKey implements Serializable { public String toString() { return tenantId + "_" + tbResourceId; } + } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index ec37a5a864..399a27f07e 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -34,11 +34,11 @@ import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; -import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueClusterService; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java index 8b7e021484..278232d2f3 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; import java.util.Optional; +import java.util.UUID; public interface EdgeService extends EntityDaoService { @@ -86,6 +87,8 @@ public interface EdgeService extends EntityDaoService { PageData findEdgesByTenantIdAndEntityId(TenantId tenantId, EntityId ruleChainId, PageLink pageLink); + PageData findEdgeIdsByTenantIdAndEntityId(TenantId tenantId, EntityId ruleChainId, PageLink pageLink); + PageData findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink); List findAllRelatedEdgeIds(TenantId tenantId, EntityId entityId); @@ -95,4 +98,5 @@ public interface EdgeService extends EntityDaoService { String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName); ListenableFuture isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String activityState); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/RelatedEdgesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/RelatedEdgesService.java new file mode 100644 index 0000000000..eef5dd9e90 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/RelatedEdgesService.java @@ -0,0 +1,30 @@ +/** + * Copyright ยฉ 2016-2024 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.dao.edge; + +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface RelatedEdgesService { + + PageData findEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + + void publishRelatedEdgeIdsEvictEvent(TenantId tenantId, EntityId entityId); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 2de57f36ac..adf626a8d0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -25,6 +25,7 @@ public class CacheConstants { public static final String USER_CACHE = "users"; public static final String ENTITY_VIEW_CACHE = "entityViews"; public static final String EDGE_CACHE = "edges"; + public static final String RELATED_EDGES_CACHE = "relatedEdges"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index b529edeb0c..9641b5e69f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -55,7 +55,7 @@ public class DataConstants { public static final String TB_IMAGE_PREFIX = "tb-image;"; - public static final String[] allScopes() { + public static String[] allScopes() { return new String[]{CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE}; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java index 99378dbc7c..9254789527 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.kv; import jakarta.validation.Valid; import lombok.Data; + import java.util.Optional; /** diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java index 82f96ea966..f0fe38996b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java @@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.kv; import jakarta.validation.Valid; import lombok.Data; -import java.util.Objects; import java.util.Optional; @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterableByTenantIdEntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterableByTenantIdEntityId.java index 48cf0e6765..d0cc471888 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterableByTenantIdEntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterableByTenantIdEntityId.java @@ -29,7 +29,6 @@ public class PageDataIterableByTenantIdEntityId extends BasePageDataIterable< this.function = function; this.tenantId = tenantId; this.entityId = entityId; - } @Override @@ -40,4 +39,5 @@ public class PageDataIterableByTenantIdEntityId extends BasePageDataIterable< public interface FetchFunction { PageData fetch(TenantId tenantId, EntityId entityId, PageLink link); } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index e03266b3e6..11b7df1958 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.queue.kafka; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -35,7 +36,6 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.TbProperty; import org.thingsboard.server.queue.util.PropertyUtils; -import javax.annotation.PreDestroy; import java.util.Collections; import java.util.List; import java.util.Map; diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java index 63806f7630..fec6185d90 100644 --- a/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java +++ b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java @@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index 32e209b2ea..9b52be0f58 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -22,7 +22,6 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java index 4945072530..0e5fb94a89 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java @@ -16,10 +16,10 @@ package org.thingsboard.script.api; import com.google.common.util.concurrent.FutureCallback; +import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import jakarta.annotation.Nullable; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java index dc83b08af4..c2851dd108 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java @@ -20,6 +20,8 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -31,8 +33,6 @@ import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index ac631408ee..df6f5f58c8 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -22,10 +22,11 @@ import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.mvel2.CompileException; import org.mvel2.ExecutionContext; import org.mvel2.MVEL; import org.mvel2.ParserContext; @@ -46,8 +47,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.Calendar; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java index 15009d67b0..f5b7dedaa3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -29,6 +30,7 @@ import java.io.Serializable; @Builder public class AssetCacheKey implements Serializable { + @Serial private static final long serialVersionUID = 4196610233744512673L; private final TenantId tenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java index 18e45d08fb..1e91d43a8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java @@ -19,11 +19,13 @@ import lombok.Data; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Data public class AssetProfileCacheKey implements Serializable { + @Serial private static final long serialVersionUID = 8220455917177676472L; private final TenantId tenantId; @@ -60,4 +62,5 @@ public class AssetProfileCacheKey implements Serializable { return tenantId + "_" + name; } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java index cabf433425..2cbd444f80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java @@ -21,12 +21,15 @@ import lombok.Getter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.EntityId; +import java.io.Serial; import java.io.Serializable; @EqualsAndHashCode @Getter @AllArgsConstructor public class AttributeCacheKey implements Serializable { + + @Serial private static final long serialVersionUID = 2013369077925351881L; private final AttributeScope scope; @@ -37,4 +40,5 @@ public class AttributeCacheKey implements Serializable { public String toString() { return "{" + entityId + "}" + scope + "_" + key; } + } 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 6f49b2a94e..88d6757de3 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 @@ -24,7 +24,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 5c7430ebc8..0cd805ee24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -304,7 +304,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb if (customer == null) { throw new DataValidationException("Can't unassign dashboards from non-existent customer!"); } - new CustomerDashboardsUnassigner(customer).removeEntities(tenantId, customer); + new CustomerDashboardsRemover(customer).removeEntities(tenantId, customer); } @Override @@ -413,11 +413,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return EntityType.DASHBOARD; } - private class CustomerDashboardsUnassigner extends PaginatedRemover { + private class CustomerDashboardsRemover extends PaginatedRemover { - private Customer customer; + private final Customer customer; - CustomerDashboardsUnassigner(Customer customer) { + CustomerDashboardsRemover(Customer customer) { this.customer = customer; } @@ -435,7 +435,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb private class CustomerDashboardsUpdater extends PaginatedRemover { - private Customer customer; + private final Customer customer; CustomerDashboardsUpdater(Customer customer) { this.customer = customer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java index 6df8f9907e..ad886b6a7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java @@ -20,11 +20,13 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Data public class DeviceProfileCacheKey implements Serializable { + @Serial private static final long serialVersionUID = 8220455917177676472L; private final TenantId tenantId; @@ -71,4 +73,5 @@ public class DeviceProfileCacheKey implements Serializable { } return tenantId + "_" + name; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index e6ba0e5e72..afca584a80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -612,12 +612,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService implements RelatedEdgesService { + + public static final int RELATED_EDGES_CACHE_ITEMS = 1000; + public static final PageLink FIRST_PAGE = new PageLink(RELATED_EDGES_CACHE_ITEMS); + + @Autowired + @Lazy + private EdgeService edgeService; + + @TransactionalEventListener(classes = RelatedEdgesEvictEvent.class) + @Override + public void handleEvictEvent(RelatedEdgesEvictEvent event) { + cache.evict(new RelatedEdgesCacheKey(event.getTenantId(), event.getEntityId())); + } + + @Override + public PageData findEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { + if (!pageLink.equals(FIRST_PAGE)) { + return edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink) + } + return cache.getAndPutInTransaction(new RelatedEdgesCacheKey(tenantId, entityId), + () -> new RelatedEdgesCacheValue(edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink)), false).getPageData(); + } + + @Override + public void publishRelatedEdgeIdsEvictEvent(TenantId tenantId, EntityId entityId) { + publishEvictEvent(new RelatedEdgesEvictEvent(tenantId, entityId)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java index f629a97a7f..be876cb5e0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -35,147 +36,40 @@ import java.util.UUID; */ public interface EdgeDao extends Dao { - /** - * Save or update edge object - * - * @param edge the edge object - * @return saved edge object - */ Edge save(TenantId tenantId, Edge edge); - /** - * Find edge info by id. - * - * @param tenantId the tenant id - * @param edgeId the edge id - * @return the edge info object - */ EdgeInfo findEdgeInfoById(TenantId tenantId, UUID edgeId); - /** - * Find edges by tenantId and page link. - * - * @param tenantId the tenantId - * @param pageLink the page link - * @return the list of edge objects - */ PageData findEdgesByTenantId(UUID tenantId, PageLink pageLink); - /** - * Find edges by tenantId, type and page link. - * - * @param tenantId the tenantId - * @param type the type - * @param pageLink the page link - * @return the list of edge objects - */ PageData findEdgesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); - /** - * Find edges by tenantId and edges Ids. - * - * @param tenantId the tenantId - * @param edgeIds the edge Ids - * @return the list of edge objects - */ ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds); - /** - * Find edges by tenantId, customerId and page link. - * - * @param tenantId the tenantId - * @param customerId the customerId - * @param pageLink the page link - * @return the list of edge objects - */ PageData findEdgesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); - /** - * Find edges by tenantId, customerId, type and page link. - * - * @param tenantId the tenantId - * @param customerId the customerId - * @param type the type - * @param pageLink the page link - * @return the list of edge objects - */ PageData findEdgesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); - /** - * Find edge infos by tenantId, customerId and page link. - * - * @param tenantId the tenantId - * @param customerId the customerId - * @param pageLink the page link - * @return the list of edge info objects - */ PageData findEdgeInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); - /** - * Find edge infos by tenantId, customerId, type and page link. - * - * @param tenantId the tenantId - * @param customerId the customerId - * @param type the type - * @param pageLink the page link - * @return the list of edge info objects - */ PageData findEdgeInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); - /** - * Find edges by tenantId, customerId and edges Ids. - * - * @param tenantId the tenantId - * @param customerId the customerId - * @param edgeIds the edge Ids - * @return the list of edge objects - */ ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List edgeIds); - /** - * Find edges by tenantId and edge name. - * - * @param tenantId the tenantId - * @param name the edge name - * @return the optional edge object - */ Optional findEdgeByTenantIdAndName(UUID tenantId, String name); - /** - * Find tenants edge types. - * - * @return the list of tenant edge type objects - */ ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId); - /** - * Find edge by routing Key. - * - * @param routingKey the edge routingKey - * @return the optional edge object - */ Optional findByRoutingKey(UUID tenantId, String routingKey); PageData findEdgeInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); PageData findEdgeInfosByTenantId(UUID tenantId, PageLink pageLink); - /** - * Find edges by tenantId and entityId. - * - * @param tenantId the tenantId - * @param entityId the entityId - * @param entityType the entityType - * @return the list of edge objects - */ PageData findEdgesByTenantIdAndEntityId(UUID tenantId, UUID entityId, EntityType entityType, PageLink pageLink); - /** - * Find edges by tenantProfileId. - * - * @param tenantProfileId the tenantProfileId - * @return the list of edge objects - */ + PageData findEdgeIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId, EntityType entityType, PageLink pageLink); + PageData findEdgesByTenantProfileId(UUID tenantProfileId, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index c03bc0a238..6a9536d8ac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -32,6 +32,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.edge.EdgeCacheEvictEvent; +import org.thingsboard.server.cache.edge.EdgeCacheKey; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; @@ -82,6 +84,7 @@ import java.util.UUID; import java.util.stream.Collectors; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -122,6 +125,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); - customerEdgeUnassigner.removeEntities(tenantId, customerId); + customerEdgeRemover.removeEntities(tenantId, customerId); } @Override @@ -433,6 +440,14 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeIdsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { + log.trace("Executing findEdgeIdsByTenantIdAndEntityId, tenantId [{}], entityId [{}], pageLink [{}]", tenantId, entityId, pageLink); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validatePageLink(pageLink); + return edgeDao.findEdgeIdsByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), entityId.getEntityType(), pageLink); + } + @Override public PageData findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink) { log.trace("Executing findEdgesByTenantProfileId, tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink); @@ -441,7 +456,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService tenantEdgesRemover = + private final PaginatedRemover tenantEdgesRemover = new PaginatedRemover<>() { @Override @@ -455,7 +470,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService customerEdgeUnassigner = new PaginatedRemover<>() { + private final PaginatedRemover customerEdgeRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { @@ -477,7 +492,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService relatedEdgeIdsIterator = - new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); + new PageDataIterableByTenantIdEntityId<>(this::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); List result = new ArrayList<>(); for (EdgeId edgeId : relatedEdgeIdsIterator) { result.add(edgeId); @@ -497,18 +512,17 @@ public class EdgeServiceImpl extends AbstractCachedEntityService edgeIds = Collections.singletonList(new EdgeId(entityId.getId())); - return new PageData<>(edgeIds, 1, 1, false); + return new PageData<>(List.of(new EdgeId(entityId.getId())), 1, 1, false); case DEVICE: case ASSET: case ENTITY_VIEW: case DASHBOARD: case RULE_CHAIN: - return convertToEdgeIds(findEdgesByTenantIdAndEntityId(tenantId, entityId, pageLink)); + return relatedEdgesService.findEdgeIdsByEntityId(tenantId, entityId, pageLink); case USER: User userById = userService.findUserById(tenantId, new UserId(entityId.getId())); if (userById == null) { - return createEmptyEdgeIdPageData(); + return PageData.emptyPageData(); } if (userById.getCustomerId() == null || userById.getCustomerId().isNullUid()) { return convertToEdgeIds(findEdgesByTenantId(tenantId, pageLink)); @@ -519,17 +533,13 @@ public class EdgeServiceImpl extends AbstractCachedEntityService createEmptyEdgeIdPageData() { - return new PageData<>(new ArrayList<>(), 0, 0, false); - } - private PageData convertToEdgeIds(PageData pageData) { if (pageData == null) { - return createEmptyEdgeIdPageData(); + return PageData.emptyPageData(); } List edgeIds = new ArrayList<>(); if (pageData.getData() != null && !pageData.getData().isEmpty()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheKey.java index a977e53b31..48df179fa7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityCountCacheKey.java @@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -28,6 +29,7 @@ import java.io.Serializable; @RequiredArgsConstructor public class EntityCountCacheKey implements Serializable { + @Serial private static final long serialVersionUID = -1992105662738434178L; private final TenantId tenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java index c5398653a0..59df403165 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -29,6 +30,9 @@ import java.io.Serializable; @Builder public class EntityViewCacheKey implements Serializable { + @Serial + private static final long serialVersionUID = 5986277528222738163L; + private final TenantId tenantId; private final String name; private final EntityId entityId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index cbe6883b46..43b5e5cdfb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -157,7 +157,7 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService INCORRECT_TENANT_ID + id); validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); - customerEntityViewsUnAssigner.removeEntities(tenantId, customerId); + customerEntityViewsRemover.removeEntities(tenantId, customerId); } @Override @@ -450,7 +450,7 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService tenantEntityViewRemover = new PaginatedRemover() { + private final PaginatedRemover tenantEntityViewRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return entityViewDao.findEntityViewsByTenantId(id.getId(), pageLink); @@ -462,7 +462,7 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService customerEntityViewsUnAssigner = new PaginatedRemover() { + private final PaginatedRemover customerEntityViewsRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { return entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java index ad0120ba53..e4381a5c36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.OtaPackageId; +import java.io.Serial; import java.io.Serializable; @Getter @@ -29,6 +30,9 @@ import java.io.Serializable; @Builder public class OtaPackageCacheKey implements Serializable { + @Serial + private static final long serialVersionUID = 6733960018642945642L; + private final OtaPackageId id; @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java index 2084bb9641..30cf46e41e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import java.io.Serial; import java.io.Serializable; @EqualsAndHashCode @@ -31,6 +32,7 @@ import java.io.Serializable; @Builder public class RelationCacheKey implements Serializable { + @Serial private static final long serialVersionUID = 3911151843961657570L; private final EntityId from; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index 2302dd0abf..d668f51dbd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; -import java.util.Collection; import java.util.List; /** @@ -71,4 +70,5 @@ public interface RelationDao { List deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java index 559214128d..b0c6a47783 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -120,6 +120,16 @@ public interface EdgeRepository extends JpaRepository { @Param("textSearch") String textSearch, Pageable pageable); + @Query("SELECT ee.id FROM EdgeEntity ee, RelationEntity re WHERE ee.tenantId = :tenantId " + + "AND ee.id = re.fromId AND re.fromType = 'EDGE' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.toId = :entityId AND re.toType = :entityType " + + "AND (:textSearch IS NULL OR ilike(ee.name, CONCAT('%', :textSearch, '%')) = true)") + Page findIdsByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT ee FROM EdgeEntity ee, TenantEntity te WHERE ee.tenantId = te.id AND te.tenantProfileId = :tenantProfileId ") Page findByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId, Pageable pageable); @@ -134,4 +144,5 @@ public interface EdgeRepository extends JpaRepository { List findEdgesByTenantIdAndIdIn(UUID tenantId, List edgeIds); EdgeEntity findByRoutingKey(String routingKey); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java index e76dfe9764..3c2d64825f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -183,6 +184,18 @@ public class JpaEdgeDao extends JpaAbstractDao implements Edge DaoUtil.toPageable(pageLink))); } + @Override + public PageData findEdgeIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId, EntityType entityType, PageLink pageLink) { + log.debug("Try to find edge ids by tenantId [{}], entityId [{}], entityType [{}], pageLink [{}]", tenantId, entityId, entityType, pageLink); + return DaoUtil.pageToPageData( + edgeRepository.findIdsByTenantIdAndEntityId( + tenantId, + entityId, + entityType.name(), + pageLink.getTextSearch(), + DaoUtil.toPageable(pageLink))).mapData(EdgeId::fromUUID); + } + @Override public PageData findEdgesByTenantProfileId(UUID tenantProfileId, PageLink pageLink) { log.debug("Try to find edges by tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java index d8637950d2..e467a36243 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java @@ -18,11 +18,13 @@ package org.thingsboard.server.dao.tenant; import lombok.Data; import org.thingsboard.server.common.data.id.TenantProfileId; +import java.io.Serial; import java.io.Serializable; @Data public class TenantProfileCacheKey implements Serializable { + @Serial private static final long serialVersionUID = 8220455917177676472L; private final TenantProfileId tenantProfileId; @@ -50,4 +52,5 @@ public class TenantProfileCacheKey implements Serializable { return tenantProfileId.toString(); } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java index adb572922a..9880f21cc1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.timeseries; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; -import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.EntityId; import java.io.Serial; @@ -28,6 +27,8 @@ import java.io.Serializable; @Getter @AllArgsConstructor public class TsLatestCacheKey implements Serializable { + + @Serial private static final long serialVersionUID = 2024369077925351881L; private final EntityId entityId; @@ -37,4 +38,5 @@ public class TsLatestCacheKey implements Serializable { public String toString() { return "{" + entityId + "}" + key; } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index ea0b6eab8d..7d8018cdd8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -42,8 +42,8 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; -import org.thingsboard.server.common.data.housekeeper.TenantEntitiesDeletionHousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; +import org.thingsboard.server.common.data.housekeeper.TenantEntitiesDeletionHousekeeperTask; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -55,12 +55,12 @@ import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; +import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.dao.audit.AuditLogLevelFilter; import org.thingsboard.server.dao.audit.AuditLogLevelMask; import org.thingsboard.server.dao.audit.AuditLogLevelProperties; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; -import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; import org.thingsboard.server.dao.tenant.TenantService; import java.io.IOException; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index 6c87b0128f..a26f345fb7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -18,28 +18,19 @@ package org.thingsboard.server.dao.service; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; -import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.ResultSetExtractor; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; -import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -108,7 +99,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java index 7919893d00..edb521d51a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java @@ -30,8 +30,6 @@ import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueStatsService; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 06b8069f11..48f7c92b1d 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -75,6 +75,9 @@ cache.specs.otaPackagesData.maxSize=100000 cache.specs.edges.timeToLiveInMinutes=1440 cache.specs.edges.maxSize=100000 +cache.specs.relatedEdges.timeToLiveInMinutes=1440 +cache.specs.relatedEdges.maxSize=100000 + cache.specs.notificationRules.timeToLiveInMinutes=1440 cache.specs.notificationRules.maxSize=10000 diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java index a43c4be9d2..4ae86742a2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java @@ -40,6 +40,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; + @Slf4j @RuleNode( type = ComponentType.ACTION, @@ -66,8 +68,6 @@ import java.util.UUID; ) public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode { - static final int DEFAULT_PAGE_SIZE = 100; - @Override EdgeEvent buildEvent(TenantId tenantId, EdgeEventActionType eventAction, UUID entityId, EdgeEventType eventType, JsonNode entityBody) { @@ -122,7 +122,7 @@ public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = new PageDataIterableByTenantIdEntityId<>( - ctx.getEdgeService()::findRelatedEdgeIdsByEntityId, ctx.getTenantId(), msg.getOriginator(), DEFAULT_PAGE_SIZE); + ctx.getEdgeService()::findRelatedEdgeIdsByEntityId, ctx.getTenantId(), msg.getOriginator(), RELATED_EDGES_CACHE_ITEMS); for (EdgeId edgeId : edgeIds) { EdgeEvent edgeEvent = buildEvent(msg, ctx); futures.add(notifyEdge(ctx, edgeEvent, edgeId)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java index 09787d3da8..eaa20e7b59 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java @@ -50,9 +50,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.mock; import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java index 48f42edb97..e8238ce5af 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java @@ -54,9 +54,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.mock; import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java index cd191c3707..db2a015f28 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java @@ -16,8 +16,8 @@ package org.thingsboard.rule.engine.credentials; import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java index b8a70991bc..621a1a4ba2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java @@ -49,6 +49,7 @@ import java.util.UUID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; @ExtendWith(MockitoExtension.class) public class TbMsgPushToEdgeNodeTest { @@ -82,7 +83,7 @@ public class TbMsgPushToEdgeNodeTest { public void ackMsgInCaseNoEdgeRelated() { Mockito.when(ctx.getTenantId()).thenReturn(tenantId); Mockito.when(ctx.getEdgeService()).thenReturn(edgeService); - Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, deviceId, new PageLink(TbMsgPushToEdgeNode.DEFAULT_PAGE_SIZE))).thenReturn(new PageData<>()); + Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, deviceId, new PageLink(RELATED_EDGES_CACHE_ITEMS))).thenReturn(new PageData<>()); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, null, null); @@ -103,7 +104,7 @@ public class TbMsgPushToEdgeNodeTest { UserId userId = new UserId(UUID.randomUUID()); EdgeId edgeId = new EdgeId(UUID.randomUUID()); PageData edgePageData = new PageData<>(List.of(edgeId), 1, 1, false); - Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, userId, new PageLink(TbMsgPushToEdgeNode.DEFAULT_PAGE_SIZE))).thenReturn(edgePageData); + Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, userId, new PageLink(RELATED_EDGES_CACHE_ITEMS))).thenReturn(edgePageData); TbMsg msg = TbMsg.newMsg(TbMsgType.ATTRIBUTES_UPDATED, userId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, null, null); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java index 84bbafe825..fb08dff8e2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; 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.junit.jupiter.api.extension.ExtendWith; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java index f4e69b9079..894a538c80 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java @@ -35,7 +35,6 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java index 1efc1c1e73..3b6b1eb3c6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java @@ -37,7 +37,6 @@ import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java index 6ecd1b6eb9..e567160b0a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java @@ -15,9 +15,9 @@ */ package org.thingsboard.rule.engine.metadata; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java index cb9c913a4e..9f20301b46 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java @@ -33,7 +33,6 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java index 66ab5826b7..b5cf5533b8 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java @@ -53,7 +53,6 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; From 458a1a0af06ae51164ec59cab8f4c2a94fb3f013 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 27 Aug 2024 12:54:29 +0300 Subject: [PATCH 292/331] Fix compile error in BaseRelatedEdgesService --- .../thingsboard/server/dao/edge/BaseRelatedEdgesService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java index 2feb4447c5..265831640c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java @@ -48,7 +48,7 @@ public class BaseRelatedEdgesService extends AbstractCachedEntityService findEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { if (!pageLink.equals(FIRST_PAGE)) { - return edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink) + return edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink); } return cache.getAndPutInTransaction(new RelatedEdgesCacheKey(tenantId, entityId), () -> new RelatedEdgesCacheValue(edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink)), false).getPageData(); From fc8631625fd63c60a83da8973d9b0ac33ccf9e4f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 27 Aug 2024 13:12:03 +0300 Subject: [PATCH 293/331] Delete unused file --- .../TbJsonTypeReferenceRedisSerializer.java | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java deleted file mode 100644 index f56f1f70b2..0000000000 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbJsonTypeReferenceRedisSerializer.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.cache; - -import com.fasterxml.jackson.core.type.TypeReference; -import org.springframework.data.redis.serializer.SerializationException; -import org.thingsboard.common.util.JacksonUtil; - -public class TbJsonTypeReferenceRedisSerializer implements TbRedisSerializer { - - private final TypeReference typeReference; - - public TbJsonTypeReferenceRedisSerializer(TypeReference typeReference) { - this.typeReference = typeReference; - } - - @Override - public byte[] serialize(V v) throws SerializationException { - return JacksonUtil.writeValueAsBytes(v); - } - - @Override - public V deserialize(K key, byte[] bytes) throws SerializationException { - return JacksonUtil.fromBytes(bytes, typeReference); - } - -} From a63ed4fa6cc2e38b855d0f1ed631addd238f359d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 27 Aug 2024 13:57:32 +0300 Subject: [PATCH 294/331] Versioning for rule chain metadata --- .../controller/RuleChainControllerTest.java | 43 +++++++++++++++++++ .../EntityVersionMismatchException.java | 6 +++ .../common/data/rule/RuleChainMetaData.java | 6 ++- .../data/sync/ie/RuleChainExportData.java | 2 +- .../server/dao/rule/BaseRuleChainService.java | 4 ++ .../server/dao/sql/JpaAbstractDao.java | 2 +- 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 949dd4a781..0aca783e67 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -30,6 +30,8 @@ import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbCreateAlarmNode; import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode; import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration; import org.thingsboard.server.common.data.StringUtils; @@ -356,6 +358,47 @@ public class RuleChainControllerTest extends AbstractControllerTest { assertThat(error).contains("alarmType is malformed"); } + @Test + public void testSaveRuleChainWithOutdatedVersion() throws Exception { + RuleChain ruleChain = createRuleChain("Rule chain with invalid nodes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("Test"); + ruleNode.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration config = new TbMsgGeneratorNodeConfiguration(); + ruleNode.setConfiguration(JacksonUtil.valueToTree(config)); + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); // in this case when saving metadata, rule chain will also be updated + + ruleChain = doGet("/api/ruleChain/" + ruleChain.getId(), RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(2); + + ruleChain.setName("Updated"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(3); + + ruleChain.setVersion(1L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isConflict()); + ruleChainMetaData.setVersion(1L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isConflict()); + + ruleChainMetaData.setVersion(3L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isOk()); + ruleChain.setVersion(3L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isOk()); + } + private RuleChain createRuleChain(String name) { RuleChain ruleChain = new RuleChain(); ruleChain.setName(name); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java index 310c6bccf7..e6ed0b824a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java @@ -15,10 +15,16 @@ */ package org.thingsboard.server.common.data.exception; +import org.thingsboard.server.common.data.EntityType; + public class EntityVersionMismatchException extends RuntimeException { public EntityVersionMismatchException(String message, Throwable cause) { super(message, cause); } + public EntityVersionMismatchException(EntityType entityType, Throwable cause) { + this((entityType != null ? entityType.getNormalName() : "Entity") + " was already changed by someone else", cause); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index eb18e28c8c..0ccbd1d34a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.RuleChainId; import java.util.ArrayList; @@ -27,11 +28,14 @@ import java.util.List; */ @Schema @Data -public class RuleChainMetaData { +public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Version of the Rule Chain") + private Long version; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java index 98eaff96c9..cccffd6fed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; public class RuleChainExportData extends EntityExportData { @JsonProperty(index = 3) - @JsonIgnoreProperties("ruleChainId") + @JsonIgnoreProperties({"ruleChainId", "version"}) private RuleChainMetaData metaData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index fea3980446..68bb185e5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -174,6 +175,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { return RuleChainUpdateResult.failed(); + } else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { + throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null); } RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); @@ -298,6 +301,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChainId); + ruleChainMetaData.setVersion(ruleChain.getVersion()); List ruleNodes = getRuleChainNodes(tenantId, ruleChainId); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Map ruleNodeIndexMap = new HashMap<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 801898386e..a7d49e244b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -79,7 +79,7 @@ public abstract class JpaAbstractDao, D> try { entity = doSave(entity, isNew, flush); } catch (OptimisticLockException e) { - throw new EntityVersionMismatchException((getEntityType() != null ? getEntityType().getNormalName() : "Entity") + " was already changed by someone else", e); + throw new EntityVersionMismatchException(getEntityType(), e); } return DaoUtil.getData(entity); } From bc0022aafda9c8d3ffc7a7efad14f27680a4aeb3 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 27 Aug 2024 15:05:39 +0200 Subject: [PATCH 295/331] Added tenant id to the methods with locks and minor refactoring --- .../DefaultSubscriptionManagerService.java | 8 +-- .../DefaultTbLocalSubscriptionService.java | 64 +++++++++---------- .../subscription/TbAbstractDataSubCtx.java | 2 +- .../subscription/TbAbstractSubCtx.java | 2 +- .../subscription/TbAlarmDataSubCtx.java | 2 +- .../subscription/TbAlarmsSubscription.java | 2 +- .../subscription/TbEntityDataSubCtx.java | 2 +- .../TbLocalSubscriptionService.java | 6 +- .../subscription/TbSubscriptionUtils.java | 6 +- .../service/ws/DefaultWebSocketService.java | 22 ++++--- .../server/service/ws/WebSocketService.java | 3 +- .../DefaultNotificationCommandsHandler.java | 2 +- common/proto/src/main/proto/queue.proto | 2 + 13 files changed, 63 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 66b73781a0..5e1cd06b62 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -118,7 +118,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } callback.onSuccess(); if (event.hasTsOrAttrSub()) { - sendSubEventCallback(serviceId, entityId, event.getSeqNumber()); + sendSubEventCallback(tenantId, serviceId, entityId, event.getSeqNumber()); } } else { log.warn("[{}][{}][{}] Event belongs to external partition. Probably re-balancing is in progress. Topic: {}" @@ -142,12 +142,12 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } - private void sendSubEventCallback(String targetId, EntityId entityId, int seqNumber) { + private void sendSubEventCallback(TenantId tenantId, String targetId, EntityId entityId, int seqNumber) { var update = getEntityUpdatesInfo(entityId); if (serviceId.equals(targetId)) { - localSubscriptionService.onSubEventCallback(entityId, seqNumber, update, TbCallback.EMPTY); + localSubscriptionService.onSubEventCallback(tenantId, entityId, seqNumber, update, TbCallback.EMPTY); } else { - sendCoreNotification(targetId, entityId, TbSubscriptionUtils.toProto(entityId.getId(), seqNumber, update)); + sendCoreNotification(targetId, entityId, TbSubscriptionUtils.toProto(tenantId, entityId.getId(), seqNumber, update)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 0d29d374a3..ceafea6553 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -213,30 +213,28 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override public void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback) { UUID entityId = new UUID(subEventCallback.getEntityIdMSB(), subEventCallback.getEntityIdLSB()); - onSubEventCallback(entityId, subEventCallback.getSeqNumber(), new TbEntityUpdatesInfo(subEventCallback.getAttributesUpdateTs(), subEventCallback.getTimeSeriesUpdateTs()), callback); + TenantId tenantId = TenantId.fromUUID(new UUID(subEventCallback.getTenantIdMSB(), subEventCallback.getTenantIdLSB())); + onSubEventCallback(tenantId, entityId, subEventCallback.getSeqNumber(), new TbEntityUpdatesInfo(subEventCallback.getAttributesUpdateTs(), subEventCallback.getTimeSeriesUpdateTs()), callback); } @Override - public void onSubEventCallback(EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { - onSubEventCallback(entityId.getId(), seqNumber, entityUpdatesInfo, callback); + public void onSubEventCallback(TenantId tenantId, EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { + onSubEventCallback(tenantId, entityId.getId(), seqNumber, entityUpdatesInfo, callback); } - public void onSubEventCallback(UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { - log.debug("[{}][{}] Processing sub event callback: {}.", entityId, seqNumber, entityUpdatesInfo); + public void onSubEventCallback(TenantId tenantId, UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { + log.debug("[{}][{}][{}] Processing sub event callback: {}.", tenantId, entityId, seqNumber, entityUpdatesInfo); entityUpdates.put(entityId, entityUpdatesInfo); Set> pendingSubs = null; - Lock subsLock = null; + Lock subsLock = getSubsLock(tenantId); + subsLock.lock(); try { TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.get(entityId); if (entitySubs != null) { - subsLock = getSubsLock(entitySubs.getTenantId()); - subsLock.lock(); pendingSubs = entitySubs.clearPendingSubscriptions(seqNumber); } } finally { - if (subsLock != null) { - subsLock.unlock(); - } + subsLock.unlock(); } if (pendingSubs != null) { pendingSubs.forEach(this::checkMissedUpdates); @@ -245,31 +243,29 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void cancelSubscription(String sessionId, int subscriptionId) { - log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); + public void cancelSubscription(TenantId tenantId, String sessionId, int subscriptionId) { + log.debug("[{}][{}][{}] Going to remove subscription.", tenantId, sessionId, subscriptionId); SubscriptionModificationResult result = null; - Lock subsLock = null; + Lock subsLock = getSubsLock(tenantId); + subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); if (sessionSubscriptions != null) { TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); if (subscription != null) { - subsLock = getSubsLock(subscription.getTenantId()); - subsLock.lock(); + if (sessionSubscriptions.isEmpty()) { subscriptionsBySessionId.remove(sessionId); } result = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); } else { - log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); + log.debug("[{}][{}][{}] Subscription not found!", tenantId, sessionId, subscriptionId); } } else { - log.debug("[{}] No session subscriptions found!", sessionId); + log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId); } } finally { - if (subsLock != null) { - subsLock.unlock(); - } + subsLock.unlock(); } if (result != null && result.hasEvent()) { pushSubscriptionEvent(result); @@ -277,27 +273,22 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void cancelAllSessionSubscriptions(String sessionId) { - log.debug("[{}] Going to remove session subscriptions.", sessionId); + public void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId) { + log.debug("[{}][{}] Going to remove session subscriptions.", tenantId, sessionId); List results = new ArrayList<>(); - Lock subsLock = null; + Lock subsLock = getSubsLock(tenantId); + subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); if (sessionSubscriptions != null) { for (TbSubscription subscription : sessionSubscriptions.values()) { - if (subsLock == null) { - subsLock = getSubsLock(subscription.getTenantId()); - subsLock.lock(); - } - results.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false)); + results.add(modifySubscription(tenantId, subscription.getEntityId(), subscription, false)); } } else { - log.debug("[{}] No session subscriptions found!", sessionId); + log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId); } } finally { - if (subsLock != null) { - subsLock.unlock(); - } + subsLock.unlock(); } results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent); } @@ -602,7 +593,12 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } private void cleanupStaleSessions() { - subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale); + subscriptionsBySessionId.forEach((sessionId, subscriptions) -> + subscriptions.values() + .stream() + .findAny() + .ifPresent(subscription -> webSocketService.cleanupIfStale(subscription.getTenantId(), sessionId)) + ); } private void handleRateLimitError(TbSubscription subscription, WebSocketSessionRef sessionRef, String message) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index 77ef960d72..1ec47beeab 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -113,7 +113,7 @@ public abstract class TbAbstractDataSubCtx { protected void clearDynamicValueSubscriptions() { if (subToDynamicValueKeySet != null) { for (Integer subId : subToDynamicValueKeySet) { - localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId); + localSubscriptionService.cancelSubscription(getTenantId(), sessionRef.getSessionId(), subId); } subToDynamicValueKeySet.clear(); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 828da388fb..93c5f4323d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -341,7 +341,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { long startTs = System.currentTimeMillis() - query.getPageLink().getTimeWindow(); newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity)); } - subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); + subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId)); subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java index d59f8d74c3..a9ab7797b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -36,7 +36,7 @@ public class TbAlarmsSubscription extends TbSubscription { } } } - subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); + subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId)); subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java index 59e7ad532d..6d742e88a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java @@ -34,11 +34,11 @@ public interface TbLocalSubscriptionService { void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback); - void onSubEventCallback(EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback empty); + void onSubEventCallback(TenantId tenantId, EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback empty); - void cancelSubscription(String sessionId, int subscriptionId); + void cancelSubscription(TenantId tenantId, String sessionId, int subscriptionId); - void cancelAllSessionSubscriptions(String sessionId); + void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId); void onTimeSeriesUpdate(TransportProtos.TbSubUpdateProto tsUpdate, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index d691b70c55..79e41ecbfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -82,10 +82,12 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder).build(); } - public static ToCoreNotificationMsg toProto(UUID id, int seqNumber, TbEntityUpdatesInfo update) { + public static ToCoreNotificationMsg toProto(TenantId tenantId, UUID id, int seqNumber, TbEntityUpdatesInfo update) { TransportProtos.TbEntitySubEventCallbackProto.Builder updateProto = TransportProtos.TbEntitySubEventCallbackProto.newBuilder() .setEntityIdMSB(id.getMostSignificantBits()) - .setEntityIdLSB(id.getLeastSignificantBits()) + .setEntityIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(id.getLeastSignificantBits()) .setSeqNumber(seqNumber) .setAttributesUpdateTs(update.attributesUpdateTs) .setTimeSeriesUpdateTs(update.timeSeriesUpdateTs); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 33b011cc0a..8d96d14b49 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -193,17 +193,18 @@ public class DefaultWebSocketService implements WebSocketService { @Override public void handleSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) { String sessionId = sessionRef.getSessionId(); + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); log.debug(PROCESSING_MSG, sessionId, event); switch (event.getEventType()) { case ESTABLISHED: wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); break; case ERROR: - log.debug("[{}] Unknown websocket session error: ", sessionId, + log.debug("[{}][{}] Unknown websocket session error: ", tenantId, sessionId, event.getError().orElse(new RuntimeException("No error specified"))); break; case CLOSED: - cleanupSessionById(sessionId); + cleanupSessionById(tenantId, sessionId); processSessionClose(sessionRef); break; } @@ -297,10 +298,10 @@ public class DefaultWebSocketService implements WebSocketService { } @Override - public void cleanupIfStale(String sessionId) { + public void cleanupIfStale(TenantId tenantId, String sessionId) { if (!msgEndpoint.isOpen(sessionId)) { log.info("[{}] Cleaning up stale session ", sessionId); - cleanupSessionById(sessionId); + cleanupSessionById(tenantId, sessionId); } } @@ -754,22 +755,23 @@ public class DefaultWebSocketService implements WebSocketService { } private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - log.warn("[{}][{}] Cleanup session due to empty entity id.", sessionId, cmd.getCmdId()); - cleanupSessionById(sessionId); + log.warn("[{}][{}][{}] Cleanup session due to empty entity id.", tenantId, sessionId, cmd.getCmdId()); + cleanupSessionById(tenantId, sessionId); } else { Integer subId = sessionCmdMap.getOrDefault(sessionId, Collections.emptyMap()).remove(cmd.getCmdId()); if (subId == null) { - log.trace("[{}][{}] Failed to lookup subscription id mapping", sessionId, cmd.getCmdId()); + log.trace("[{}][{}][{}] Failed to lookup subscription id mapping", tenantId, sessionId, cmd.getCmdId()); subId = cmd.getCmdId(); } - oldSubService.cancelSubscription(sessionId, subId); + oldSubService.cancelSubscription(tenantId, sessionId, subId); } } - private void cleanupSessionById(String sessionId) { + private void cleanupSessionById(TenantId tenantId, String sessionId) { wsSessionsMap.remove(sessionId); - oldSubService.cancelAllSessionSubscriptions(sessionId); + oldSubService.cancelAllSessionSubscriptions(tenantId, sessionId); sessionCmdMap.remove(sessionId); entityDataSubService.cancelAllSessionSubscriptions(sessionId); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java index 284431ff84..00fd19780d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.ws; import org.springframework.web.socket.CloseStatus; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; @@ -37,6 +38,6 @@ public interface WebSocketService { void close(String sessionId, CloseStatus status); - void cleanupIfStale(String sessionId); + void cleanupIfStale(TenantId tenantId, String sessionId); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index 5bfe976194..84f3698845 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -242,7 +242,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH @Override public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { - localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId()); + localSubscriptionService.cancelSubscription(sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSessionId(), cmd.getCmdId()); } private void sendUpdate(String sessionId, CmdUpdate update) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 581b993120..2be1254a1a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -817,6 +817,8 @@ message TbEntitySubEventCallbackProto { int32 seqNumber = 3; int64 attributesUpdateTs = 4; int64 timeSeriesUpdateTs = 5; + int64 tenantIdMSB = 6; + int64 tenantIdLSB = 7; } message TsValueProto { From 4753455941e838dadfebef2391688fd096da2cb9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 11:17:10 +0300 Subject: [PATCH 296/331] UI: Improved move all widget action - fixed overlap and add support dashboard layout --- .../core/services/dashboard-utils.service.ts | 19 +++++++++++++++++-- .../dashboard-page.component.ts | 7 ++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 8d599ad925..5b98c7cb7b 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -752,10 +752,25 @@ export class DashboardUtilsService { public moveWidgets(layout: DashboardLayout, cols: number, rows: number) { cols = isDefinedAndNotNull(cols) ? Math.round(cols) : 0; rows = isDefinedAndNotNull(rows) ? Math.round(rows) : 0; + if (cols < 0 || rows < 0) { + let widgetMinCol = Infinity; + let widgetMinRow = Infinity; + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + widgetMinCol = Math.min(widgetMinCol, widget.col); + widgetMinRow = Math.min(widgetMinRow, widget.row); + } + if ((cols + widgetMinCol) < 0 ){ + cols = -widgetMinCol; + } + if ((rows + widgetMinRow) < 0 ){ + rows = -widgetMinRow; + } + } for (const w of Object.keys(layout.widgets)) { const widget = layout.widgets[w]; - widget.col = Math.max(0, widget.col + cols); - widget.row = Math.max(0, widget.row + rows); + widget.col += cols; + widget.row += rows; } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0d379b1623..ee29dffb59 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -988,7 +988,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }); } - private moveWidgets($event: Event, layoutId: DashboardLayoutId) { + private moveWidgets($event: Event, layoutId: DashboardLayoutId, breakpointId: BreakpointId) { if ($event) { $event.stopPropagation(); } @@ -1001,7 +1001,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }).afterClosed().subscribe((result) => { this.layouts[layoutId].layoutCtx.displayGrid = 'onDrag&Resize'; if (result) { - const targetLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId]; + const dashboardLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId]; + const targetLayout = this.dashboardUtils.getDashboardLayoutConfig(dashboardLayout, breakpointId); this.dashboardUtils.moveWidgets(targetLayout, result.cols, result.rows); this.updateLayouts(); } else { @@ -1582,7 +1583,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC dashboardContextActions.push( { action: ($event) => { - this.moveWidgets($event, layoutCtx.id); + this.moveWidgets($event, layoutCtx.id, layoutCtx.breakpoint); }, enabled: true, value: 'dashboard.move-all-widgets', From b26bf1410ce9e863d91c3a6509231d2024c2eaa8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 28 Aug 2024 10:24:22 +0200 Subject: [PATCH 297/331] fixed TbEntitySubEventCallbackProto build --- .../server/service/subscription/TbSubscriptionUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 79e41ecbfe..3d0037caba 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -85,9 +85,9 @@ public class TbSubscriptionUtils { public static ToCoreNotificationMsg toProto(TenantId tenantId, UUID id, int seqNumber, TbEntityUpdatesInfo update) { TransportProtos.TbEntitySubEventCallbackProto.Builder updateProto = TransportProtos.TbEntitySubEventCallbackProto.newBuilder() .setEntityIdMSB(id.getMostSignificantBits()) - .setEntityIdLSB(tenantId.getId().getLeastSignificantBits()) + .setEntityIdLSB(id.getLeastSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(id.getLeastSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setSeqNumber(seqNumber) .setAttributesUpdateTs(update.attributesUpdateTs) .setTimeSeriesUpdateTs(update.timeSeriesUpdateTs); From 83059c77afd8b2697040cc43d0a37054ad688b26 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 28 Aug 2024 12:28:31 +0300 Subject: [PATCH 298/331] UI: SCADA symbol stand filter --- .../system/scada_symbols/stand-filter.svg | 648 ++++++++++++++++++ .../scada_water_system_symbols.json | 1 + .../assets/locale/locale.constant-en_US.json | 16 +- 3 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/json/system/scada_symbols/stand-filter.svg diff --git a/application/src/main/data/json/system/scada_symbols/stand-filter.svg b/application/src/main/data/json/system/scada_symbols/stand-filter.svg new file mode 100644 index 0000000000..87d578605d --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/stand-filter.svg @@ -0,0 +1,648 @@ + +{ + "title": "Stand filter", + "description": "Stand filter with configurable filtration mode option and various states.", + "searchTags": [ + "filter" + ], + "widgetSizeX": 3, + "widgetSizeY": 5, + "stateRenderFunction": "", + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.standFilterColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "filterMode", + "stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var i = 0;\n if (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n }\n if (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n }\n if (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n }\n if (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n }\n if (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n }\n if (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n }\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n if (isFinite(filtrationMode)) {\n setFilterModeColorsByMap(filtrationMode, running);\n }\n}\n\nif (running) {\n ctx.api.enable(element.children());\n} else {\n ctx.api.disable(element.children());\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "filtrationMode", + "name": "{i18n:scada.symbol.filtration-mode}", + "hint": "{i18n:scada.symbol.filtration-mode-hint}", + "group": null, + "type": "value", + "valueType": "INTEGER", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "filtrationMode" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "filtrationModeUpdateState", + "name": "{i18n:scada.symbol.filtration-mode-update}", + "hint": "{i18n:scada.symbol.filtration-mode-update-hint}", + "group": null, + "type": "action", + "valueType": "INTEGER", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "filtrationMode" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "VALUE", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "filtrationMode", + "name": "{i18n:scada.symbol.filter-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "wasteMode", + "name": "{i18n:scada.symbol.waste-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backwashMode", + "name": "{i18n:scada.symbol.backwash-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "recirculateMode", + "name": "{i18n:scada.symbol.recirculate-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "rinseMode", + "name": "{i18n:scada.symbol.rinse-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedMode", + "name": "{i18n:scada.symbol.closed-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "standFilterColor", + "name": "{i18n:scada.symbol.stand-filter-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "modeBoxBackground", + "name": "{i18n:scada.symbol.mode-box-background}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.border-color}", + "type": "color", + "default": "#198038", + "required": null, + "subLabel": "Active", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "column-xs", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.border-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "Default", + "divider": false, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeLabelColor", + "name": "{i18n:scada.symbol.label-color}", + "type": "color", + "default": "#000000C2", + "required": null, + "subLabel": "Active", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "column-xs", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultLabelColor", + "name": "{i18n:scada.symbol.label-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "Default", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + Filter + + + + + + Backwash + + + + + + Rinse + + + + + + Waste + + + + + + Recirculate + + + + + + Closed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json index 29efb21bc6..6f0edcd02f 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -45,6 +45,7 @@ "long_bottom_filter", "short_top_filter", "long_top_filter", + "stand_filter", "horizontal_wheel_valve", "vertical_wheel_valve", "horizontal_ball_valve", 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 a447f3290b..c6d2164a5a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3828,7 +3828,21 @@ "heat-pump-color": "Heat pump color", "power-button-background": "Power button background", "value-box-background": "Value box background", - "value-units": "Value units" + "value-units": "Value units", + "filtration-mode": "Filtration mode", + "filtration-mode-hint": "Integer value indication the current filtration mode.", + "filtration-mode-update": "Filtration mode update state", + "filtration-mode-update-hint": "Action invoked when user clicks to change current filtration mode.", + "filter-mode": "Filter", + "waste-mode": "Waste", + "backwash-mode": "Backwash", + "recirculate-mode": "Recirculate", + "rinse-mode": "Rinse", + "closed-mode": "Closed", + "stand-filter-color": "Stand filter color", + "mode-box-background": "Mode box background", + "border-color": "Border color", + "label-color": "Label color" } }, "item": { From 6b00fb49002e0556408660f49349b57678389c92 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 Aug 2024 12:32:11 +0300 Subject: [PATCH 299/331] Increment entity's version regardless of the changes --- .../controller/DeviceControllerTest.java | 3 +- .../controller/RuleChainControllerTest.java | 12 ++--- .../server/dao/rule/BaseRuleChainService.java | 4 +- .../server/dao/sql/JpaAbstractDao.java | 19 +++++--- .../dao/sql/device/JpaDeviceDaoTest.java | 44 +++++++++++++++++-- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index cf46d0f7fd..c036f70e4a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -1503,11 +1503,12 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); Device updatedDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); + savedDevice.setVersion(updatedDevice.getVersion()); Assert.assertEquals(savedDevice, updatedDevice); DeviceCredentials updatedCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); - + savedCredentials.setVersion(updatedCredentials.getVersion()); Assert.assertEquals(savedCredentials, updatedCredentials); } diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 0aca783e67..3d85c4cc12 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -360,7 +360,7 @@ public class RuleChainControllerTest extends AbstractControllerTest { @Test public void testSaveRuleChainWithOutdatedVersion() throws Exception { - RuleChain ruleChain = createRuleChain("Rule chain with invalid nodes"); + RuleChain ruleChain = createRuleChain("My rule chain"); RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChain.getId()); @@ -392,11 +392,11 @@ public class RuleChainControllerTest extends AbstractControllerTest { .andExpect(status().isConflict()); ruleChainMetaData.setVersion(3L); - doPost("/api/ruleChain/metadata", ruleChainMetaData) - .andExpect(status().isOk()); - ruleChain.setVersion(3L); - doPost("/api/ruleChain", ruleChain) - .andExpect(status().isOk()); + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(4); + ruleChain.setVersion(4L); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(5); } private RuleChain createRuleChain(String name) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 68bb185e5f..902bf0f6a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -168,7 +168,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true); } - + @Transactional @Override public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); @@ -237,7 +237,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { ruleChain.setFirstRuleNodeId(firstRuleNodeId); - ruleChainDao.save(tenantId, ruleChain); } if (ruleChainMetaData.getConnections() != null) { for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) { @@ -286,6 +285,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } + ruleChain = ruleChainDao.save(tenantId, ruleChain); if (publishSaveEvent) { eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain).entityId(ruleChain.getId()).build()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index a7d49e244b..f044629364 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -97,20 +97,28 @@ public abstract class JpaAbstractDao, D> if (versionedEntity.getVersion() == null) { HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid()); if (existingEntity != null) { - versionedEntity.setVersion(existingEntity.getVersion()); // manually resetting the version to latest to allow force overwrite of the entity + /* + * manually resetting the version to latest to allow force overwrite of the entity + * */ + versionedEntity.setVersion(existingEntity.getVersion()); } else { return doSave(entity, true, flush); } } - entity = entityManager.merge(entity); + versionedEntity = entityManager.merge(versionedEntity); + /* + * by default, Hibernate doesn't issue an update query and thus version increment + * if the entity was not modified. to bypass this and always increment the version, we do it manually + * */ + versionedEntity.setVersion(versionedEntity.getVersion() + 1); /* - * flushing so that the query is executed right away and the version is incremented, - * then removing the entity from the persistence context so that it is not affected + * flushing and then removing the entity from the persistence context so that it is not affected * by next flushes (e.g. when a transaction is committed) to avoid double version increment * */ entityManager.flush(); - entityManager.detach(entity); + entityManager.detach(versionedEntity); flushed = true; + entity = (E) versionedEntity; } else { entity = entityManager.merge(entity); } @@ -161,6 +169,7 @@ public abstract class JpaAbstractDao, D> log.debug("Remove request: {}", id); } + @Override @Transactional public void removeAllByIds(Collection ids) { JpaRepository repository = getRepository(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java index 644ce40bb8..90510cf2b7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -105,7 +106,37 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { @Test public void testSaveDeviceName0x00_thenSomeDatabaseException() { Device device = getDevice(tenantId1, customerId1, "\u0000"); - assertThatThrownBy(() -> deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId1), device).getUuidId())); + assertThatThrownBy(() -> deviceIds.add(saveDevice(tenantId1, device).getUuidId())); + } + + @Test + public void testSaveDevice_versionIncrement() { + Device device = getDevice(tenantId1, customerId1, "1ewfewf2"); + device = saveDevice(tenantId1, device); + deviceIds.add(device.getUuidId()); + assertThat(device.getVersion()).isEqualTo(1); + + device.setName(device.getName() + "x"); + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(2); + + device.setName(device.getName() + "x"); + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(3); + } + + @Test + public void testSaveDevice_versionIncrement_noChanges() { + Device device = getDevice(tenantId1, customerId1, "1ewfewf2"); + device = saveDevice(tenantId1, device); + deviceIds.add(device.getUuidId()); + assertThat(device.getVersion()).isEqualTo(1); + + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(2); + + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(3); } @Test @@ -126,7 +157,7 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { UUID customerId = Uuids.timeBased(); // send to method getDevice() number = 40, because make random name is bad and name "SEARCH_TEXT_40" don't used Device device = getDevice(tenantId, customerId, 40); - deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId), device).getUuidId()); + deviceIds.add(saveDevice(tenantId, device).getUuidId()); UUID uuid = device.getId().getId(); Device entity = deviceDao.findById(TenantId.fromUUID(tenantId), uuid); @@ -156,8 +187,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { private List createDevices(UUID tenantId1, UUID tenantId2, UUID customerId1, UUID customerId2, int count) { List savedDevicesUUID = new ArrayList<>(); for (int i = 0; i < count / 2; i++) { - savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1, i)).getUuidId()); - savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); + savedDevicesUUID.add(saveDevice(tenantId1, getDevice(tenantId1, customerId1, i)).getUuidId()); + savedDevicesUUID.add(saveDevice(tenantId2, getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); } return savedDevicesUUID; } @@ -175,4 +206,9 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { device.setDeviceProfileId(savedDeviceProfile.getId()); return device; } + + private Device saveDevice(UUID tenantId, Device device) { + return deviceDao.save(TenantId.fromUUID(tenantId), device); + } + } From d656ab5540826570a506f5c5cb3753e21a9c4f9c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 Aug 2024 12:33:17 +0300 Subject: [PATCH 300/331] Remove unneeded comment --- .../thingsboard/server/controller/RuleChainControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 3d85c4cc12..4edc3de01e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -375,7 +375,7 @@ public class RuleChainControllerTest extends AbstractControllerTest { ruleChainMetaData.setNodes(ruleNodes); ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); - assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); // in this case when saving metadata, rule chain will also be updated + assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); ruleChain = doGet("/api/ruleChain/" + ruleChain.getId(), RuleChain.class); assertThat(ruleChain.getVersion()).isEqualTo(2); From 1ea53f36b418760e20968e12035d7d521640c4da Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 28 Aug 2024 12:51:43 +0300 Subject: [PATCH 301/331] UI: Scada drain pipe change default fluid color --- .../main/data/json/system/scada_symbols/left-drain-pipe.svg | 4 ++-- .../data/json/system/scada_symbols/left-elbow-drain-pipe.svg | 4 ++-- .../main/data/json/system/scada_symbols/right-drain-pipe.svg | 4 ++-- .../data/json/system/scada_symbols/right-elbow-drain-pipe.svg | 4 ++-- .../data/json/system/scada_symbols/short-left-drain-pipe.svg | 4 ++-- .../data/json/system/scada_symbols/short-right-drain-pipe.svg | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg index 2a5c3ce47a..326a263208 100644 --- a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg @@ -94,7 +94,7 @@ "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#8CAA5C", + "default": "#518DA0", "required": null, "subLabel": null, "divider": null, @@ -130,7 +130,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg index 2f91847b71..b9a6aa6057 100644 --- a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg @@ -94,7 +94,7 @@ "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#8CAA5C", + "default": "#518DA0", "required": null, "subLabel": null, "divider": null, @@ -136,7 +136,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg index 3fe3501e16..093078c69b 100644 --- a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg @@ -93,7 +93,7 @@ "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#8CAA5C", + "default": "#518DA0", "required": null, "subLabel": null, "divider": null, @@ -129,7 +129,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg index 14ede98e81..63b143eeef 100644 --- a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg @@ -94,7 +94,7 @@ "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#8CAA5C", + "default": "#518DA0", "required": null, "subLabel": null, "divider": null, @@ -135,7 +135,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg index ffe74a681e..abf43aacb9 100644 --- a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg @@ -94,7 +94,7 @@ "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#8CAA5C", + "default": "#518DA0", "required": null, "subLabel": null, "divider": null, @@ -130,7 +130,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg index 306898e3ae..1b2ae3934a 100644 --- a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg @@ -94,7 +94,7 @@ "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#8CAA5C", + "default": "#518DA0", "required": null, "subLabel": null, "divider": null, @@ -130,7 +130,7 @@ - + From cbe5150eda97834cf2587b2d271793509c25e403 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 14:44:20 +0300 Subject: [PATCH 302/331] UI: Fixed error tooltip for 'Minimum layout width' in layout configuration --- .../dashboard-settings-dialog.component.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index f6b427cd3e..1c3ffbc922 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -145,15 +145,16 @@
dashboard.min-layout-width
- + warning @@ -190,7 +191,7 @@
dashboard.row-height
- + @@ -245,7 +246,7 @@
dashboard.mobile-row-height
- + From f598e2fe8938a4437cefe96196d82ef1d4c8496c Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 28 Aug 2024 15:20:55 +0300 Subject: [PATCH 303/331] tbel: fix bug send From Client CollectedValue --- .../lwm2m/client/LwM2MTestClient.java | 5 +- .../lwm2m/client/LwM2mTemperatureSensor.java | 66 +++++++++++++++- .../rpc/sql/RpcLwm2mIntegrationReadTest.java | 77 ++++++++++++++++++- .../lwm2m/server/LwM2mServerListener.java | 3 +- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 50 ++++++------ .../server/uplink/LwM2mUplinkMsgHandler.java | 4 +- 6 files changed, 174 insertions(+), 31 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 02ff8be9d4..b79399170e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -189,7 +189,9 @@ public class LwM2MTestClient { locationParams = new LwM2MLocationParams(); locationParams.getPos(); initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0)); - initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0), new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12)); + LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0); + LwM2mTemperatureSensor lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12); + initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2mTemperatureSensor12); List enablers = initializer.createAll(); @@ -314,6 +316,7 @@ public class LwM2MTestClient { clientDtlsCid = new HashMap<>(); clientStates.add(ON_INIT); leshanClient = builder.build(); + lwM2mTemperatureSensor12.setLeshanClient(leshanClient); LwM2mClientObserver observer = new LwM2mClientObserver() { @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java index fc75342ff8..cfdef139c2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java @@ -16,9 +16,13 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.send.ManualDataSender; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; @@ -26,6 +30,7 @@ import org.eclipse.leshan.core.response.ReadResponse; import javax.security.auth.Destroyable; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; @@ -39,6 +44,9 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr private double currentTemp = 20d; private double minMeasuredValue = currentTemp; private double maxMeasuredValue = currentTemp; + + private LeshanClient leshanClient; + private List containingValues; protected static final Random RANDOM = new Random(); private static final List supportedResources = Arrays.asList(5601, 5602, 5700, 5701); @@ -65,7 +73,17 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr case 5602: return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue)); case 5700: - return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp)); + if (identity == LwM2mServer.SYSTEM) { + setTemperature(); + setData(); + return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp)); + } else if (this.getId() == 12 && this.leshanClient != null) { + containingValues = new ArrayList<>(); + sendCollected(5700); + return ReadResponse.success(resourceId, getData()); + } else { + return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp)); + } case 5701: return ReadResponse.success(resourceId, UNIT_CELSIUS); default: @@ -91,8 +109,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } private void adjustTemperature() { - float delta = (RANDOM.nextInt(20) - 10) / 10f; - currentTemp += delta; + setTemperature(); Integer changedResource = adjustMinMaxMeasuredValue(currentTemp); fireResourceChange(5700); if (changedResource != null) { @@ -100,6 +117,10 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } } + private void setTemperature(){ + float delta = (RANDOM.nextInt(20) - 10) / 10f; + currentTemp += delta; + } private synchronized Integer adjustMinMaxMeasuredValue(double newTemperature) { if (newTemperature > maxMeasuredValue) { maxMeasuredValue = newTemperature; @@ -122,9 +143,48 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr return supportedResources; } + protected void setLeshanClient(LeshanClient leshanClient){ + this.leshanClient = leshanClient; + } + @Override public void destroy() { } + private void sendCollected(int resourceId) { + try { + LwM2mServer registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next(); + ManualDataSender sender = this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME, + ManualDataSender.class); + sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId))); + Thread.sleep(1000); + sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId))); + sender.sendCollectedData(registeredServer, ContentFormat.SENML_JSON, 1000, false); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + private LwM2mPath getPathForCollectedValue(int resourceId) { + return new LwM2mPath(3303, this.getId(), resourceId); + } + + private double getData() { + if (containingValues.size() > 1) { + Integer t0 = Math.toIntExact(Math.round(containingValues.get(0) * 100)); + Integer t1 = Math.toIntExact(Math.round(containingValues.get(1) * 100)); + long to_t1 = (((long) t0) << 32) | (t1 & 0xffffffffL); + return Double.longBitsToDouble(to_t1); + } else { + return currentTemp; + } + + } + private void setData() { + if (containingValues == null){ + containingValues = new ArrayList<>(); + } + containingValues.add(getTwoDigitValue(currentTemp)); + } } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 52e32f2d55..6d4e11ffc1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -16,19 +16,34 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; +import org.eclipse.leshan.server.registration.Registration; import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; +import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; + +import java.time.Instant; +import java.util.Map; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11; @@ -41,9 +56,13 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; - +@Slf4j public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest { + @SpyBean + DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; + + /** * Read {"id":"/3"} * Read {"id":"/6"}... @@ -57,7 +76,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest String expectedObjectId = pathIdVerToObjectId((String) expected); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; if (expectedPath.getObjectId() == 1) { expectedObjectInstances = "LwM2mObject [id=1, instances={1="; @@ -211,6 +230,60 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest assertTrue(actualValues.contains(expected19_1_0)); } + + /** + * /3303/0/5700 + * Read {"id":"/3303/0/5700"} + * Trigger a Send operation from the client with multiple values for the same resource as a payload + * acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes] + * 2 values for the resource /3303/12/5700 should be stored with timestamps1 = Instance.now(), timestamps2 = Instance.now() + * + * @throws Exception + */ + @Test + public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception { + TimestampedLwM2mNodes[] tsNodesHolder = new TimestampedLwM2mNodes[1]; + doAnswer(inv -> { + tsNodesHolder[0] = inv.getArgument(1); + return null; + }).when(defaultUplinkMsgHandlerTest).onUpdateValueWithSendRequest( + Mockito.any(Registration.class), + Mockito.any(TimestampedLwM2mNodes.class) + ); + int resourceId = 5700; + String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId; + String actualResult = sendRPCById(expectedIdVer); + verify(defaultUplinkMsgHandlerTest, timeout(10000).times(1)) + .onUpdateValueWithSendRequest(Mockito.any(Registration.class), Mockito.any(TimestampedLwM2mNodes.class)); + + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = "LwM2mSingleResource [id=" + resourceId + ", value="; + String actual = rpcActualResult.get("value").asText(); + assertTrue(actual.contains(expected)); + int indStart = actual.indexOf(expected) + expected.length(); + int indEnd = actual.indexOf(",", indStart); + String valStr = actual.substring(indStart, indEnd); + double dd = Double.parseDouble(valStr); + long combined = Double.doubleToRawLongBits(dd); + int t0 = (int) (combined >> 32); + int t1 = (int) combined; + double[] expectedValues ={(double)t0/100, (double)t1/100}; + int ind = 0; + LwM2mPath expectedPath = new LwM2mPath("/3303/12/5700"); + for (Instant ts : tsNodesHolder[0].getTimestamps()) { + Map nodesAt = tsNodesHolder[0].getNodesAt(ts); + for (var instant : nodesAt.entrySet()) { + LwM2mPath actualPath = instant.getKey(); + LwM2mNode node = instant.getValue(); + LwM2mResource lwM2mResource = (LwM2mResource) node; + assertEquals(expectedPath, actualPath); + assertEquals(expectedValues[ind], lwM2mResource.getValue()); + ind++; + } + } + } + /** * ReadComposite {"keys":["batteryLevel", "UtfOffset", "dataDescription"]} */ diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index 7797242b51..188344a66c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -137,8 +137,9 @@ public class LwM2mServerListener { @Override public void dataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request) { + log.trace("Received Send request from [{}] containing value: [{}], coapRequest: [{}]", registration.getEndpoint(), data.toString(), request.getCoapRequest().toString()); if (registration != null) { - service.onUpdateValueWithSendRequest(registration, request); + service.onUpdateValueWithSendRequest(registration, data); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index bfc9f32dd8..5be27be03d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -36,6 +36,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.CreateRequest; @@ -102,6 +103,7 @@ import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mSecurityStore; import org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -145,7 +147,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mUplinkMsgHandler { @Getter - private final LwM2mValueConverter converter = LwM2mValueConverterImpl.getInstance();; + private final LwM2mValueConverter converter = LwM2mValueConverterImpl.getInstance(); private final TransportService transportService; private final LwM2mTransportContext context; @@ -360,29 +362,33 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * Sending updated value to thingsboard from SendListener.dataReceived: object, instance, SingleResource or MultipleResource * * @param registration - Registration LwM2M Client - * @param sendRequest - sendRequest + * @param data - TimestampedLwM2mNodes (send From Client CollectedValue) */ @Override - public void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest) { - for(var entry : sendRequest.getTimestampedNodes().getNodes().entrySet()) { - LwM2mPath path = entry.getKey(); - LwM2mNode node = entry.getValue(); - LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient); - ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider); - if (objectModelVersion != null) { - if (node instanceof LwM2mObject) { - LwM2mObject lwM2mObject = (LwM2mObject) node; - this.updateObjectResourceValue(lwM2MClient, lwM2mObject, stringPath, 0); - } else if (node instanceof LwM2mObjectInstance) { - LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) node; - this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, stringPath, 0); - } else if (node instanceof LwM2mResource) { - LwM2mResource lwM2mResource = (LwM2mResource) node; - this.updateResourcesValue(lwM2MClient, lwM2mResource, stringPath, Mode.UPDATE, 0); + public void onUpdateValueWithSendRequest(Registration registration, TimestampedLwM2mNodes data) { + for (Instant ts : data.getTimestamps()) { + Map nodesAt = data.getNodesAt(ts); + for (var instant : nodesAt.entrySet()) { + LwM2mPath path = instant.getKey(); + LwM2mNode node = instant.getValue(); + LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); + String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient); + ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider); + if (objectModelVersion != null) { + if (node instanceof LwM2mObject) { + LwM2mObject lwM2mObject = (LwM2mObject) node; + this.updateObjectResourceValue(lwM2MClient, lwM2mObject, stringPath, 0); + } else if (node instanceof LwM2mObjectInstance) { + LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) node; + this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, stringPath, 0); + } else if (node instanceof LwM2mResource) { + LwM2mResource lwM2mResource = (LwM2mResource) node; + this.updateResourcesValue(lwM2MClient, lwM2mResource, stringPath, Mode.UPDATE, 0); + } } + + tryAwake(lwM2MClient); } - tryAwake(lwM2MClient); } } @@ -969,9 +975,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } - public LwM2mClientContext getClientContext(){ + public LwM2mClientContext getClientContext() { return this.clientContext; - }; + } private Map getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) { Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 0896963be2..4e8c3d397c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.transport.lwm2m.server.uplink; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.CreateRequest; -import org.eclipse.leshan.core.request.SendRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.response.ReadCompositeResponse; @@ -50,7 +50,7 @@ public interface LwM2mUplinkMsgHandler { void onUpdateValueAfterReadCompositeResponse(Registration registration, ReadCompositeResponse response); void onErrorObservation(Registration registration, String errorMsg); - void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest); + void onUpdateValueWithSendRequest(Registration registration, TimestampedLwM2mNodes data); void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile); From 7e7656f7b439b6b648640a2ec4523849b40b8b39 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 16:32:09 +0300 Subject: [PATCH 304/331] UI: Fixed infinite load when select divided layout --- .../core/services/dashboard-utils.service.ts | 19 +++++++++++++++++ .../dashboard-page.component.ts | 4 ++-- .../layout/dashboard-layout.component.ts | 21 ------------------- .../dashboard-page/layout/layout.models.ts | 1 - .../select-dashboard-breakpoint.component.ts | 2 +- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 8d599ad925..5fdf6f8d62 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -59,6 +59,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; import { MediaBreakpoints } from '@shared/models/constants'; import { TranslateService } from '@ngx-translate/core'; +import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models'; @Injectable({ providedIn: 'root' @@ -1066,4 +1067,22 @@ export class DashboardUtilsService { const maxStr = isDefined(currentData?.maxWidth) ? `max ${currentData.maxWidth}px` : ''; return minStr && maxStr ? `${minStr} - ${maxStr}` : `${minStr}${maxStr}`; } + + updatedLayoutForBreakpoint(layout: DashboardPageLayout, breakpointId: BreakpointId) { + let selectBreakpointId: BreakpointId = 'default'; + if (layout.layoutCtx.layoutData[breakpointId]) { + selectBreakpointId = breakpointId; + } + layout.layoutCtx.breakpoint = selectBreakpointId; + const layoutInfo = layout.layoutCtx.layoutData[selectBreakpointId]; + if (layoutInfo.gridSettings) { + layout.layoutCtx.gridSettings = layoutInfo.gridSettings; + } + layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); + layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; + if (layout.show && layout.layoutCtx.ctrl) { + layout.layoutCtx.ctrl.reload(); + } + layout.layoutCtx.ignoreLoading = true; + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0d379b1623..bb5c2cb796 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -450,7 +450,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }), skip(1) ).subscribe(() => { - this.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(); + this.dashboardUtils.updatedLayoutForBreakpoint(this.layouts.main, this.dashboardCtx.breakpoint); this.updateLayoutSizes(); } ) @@ -1129,7 +1129,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { layout.layoutCtx.layoutData = layoutInfo; layout.layoutCtx.layoutDataChanged.next(); - layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show); + this.dashboardUtils.updatedLayoutForBreakpoint(layout, this.isEdit ? layout.layoutCtx.breakpoint : this.dashboardCtx.breakpoint); this.updateLayoutSizes(); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index 79eef476b3..9646152eba 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -332,25 +332,4 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo const pos = this.dashboard.getEventGridPosition($event); this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos); } - - updatedCurrentBreakpoint(breakpointId?: BreakpointId, showLayout = true) { - if (!isNotEmptyStr(breakpointId)) { - breakpointId = this.dashboardCtx.breakpoint; - } - if (this.layoutCtx.layoutData[breakpointId]) { - this.layoutCtx.breakpoint = breakpointId; - } else { - this.layoutCtx.breakpoint = 'default'; - } - const layoutInfo = this.layoutCtx.layoutData[this.layoutCtx.breakpoint]; - if (layoutInfo.gridSettings) { - this.layoutCtx.gridSettings = layoutInfo.gridSettings; - } - this.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); - this.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; - if (showLayout && this.layoutCtx.ctrl) { - this.layoutCtx.ctrl.reload(); - } - this.layoutCtx.ignoreLoading = true; - } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts index 6aa69c51af..3ea9f553a8 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts @@ -21,7 +21,6 @@ export interface ILayoutController { selectWidget(widgetId: string, delay?: number); pasteWidget($event: MouseEvent); pasteWidgetReference($event: MouseEvent); - updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean); } export enum LayoutWidthType { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts index 130d0e02b2..6366154d80 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts @@ -63,7 +63,7 @@ export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy { } selectLayoutChanged() { - this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectedBreakpoint); + this.dashboardUtils.updatedLayoutForBreakpoint(this.dashboardCtrl.layouts.main, this.selectedBreakpoint); this.dashboardCtrl.updateLayoutSizes(); } From 897fe09c5ddf2d6ebe72af4d87df3e48147b86bc Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 16:50:58 +0300 Subject: [PATCH 305/331] UI: Improved style for select dashboard breakpoint --- .../layout/add-new-breakpoint-dialog.component.html | 4 ++-- .../layout/select-dashboard-breakpoint.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html index 8baab0f295..15b7ee173a 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html @@ -30,7 +30,7 @@ layout.breakpoint {{ getName(this.addBreakpointFormGroup.get('newBreakpointId').value) }} - + {{ getIcon(breakpoint) }}
{{ getName(breakpoint) }}
{{ getSizeDescription(breakpoint) }}
@@ -41,7 +41,7 @@ layout.copy-from {{ getName(this.addBreakpointFormGroup.get('copyFrom').value) }} - + {{ getIcon(breakpoint) }}
{{ getName(breakpoint) }}
{{ getSizeDescription(breakpoint) }}
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html index 417c8d9e26..790835352d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html @@ -21,7 +21,7 @@ [(ngModel)]="selectedBreakpoint" (ngModelChange)="selectLayoutChanged()"> {{ getName(selectedBreakpoint) }} - + {{ getIcon(breakpointId) }}
{{ getName(breakpointId) }}
{{ getSizeDescription(breakpointId) }}
From 6c1b03dc47203c9918e7ca03e0a5ec57b1d22279 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 17:57:47 +0300 Subject: [PATCH 306/331] UI: Fixed incorrect min-width calculation for dashboard layout in edit preview mode at specific breakpoints --- .../dashboard-page.component.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0d379b1623..5edd69bb7f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -759,6 +759,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC const prevMainLayoutWidth = this.mainLayoutSize.width; const prevMainLayoutHeight = this.mainLayoutSize.height; const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth; + const prevMainLayoutMinWidth = this.mainLayoutSize.minWidth; if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') { this.mainLayoutSize.width = '100%'; } else { @@ -773,19 +774,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC const xOffset = this.dashboardContainer.nativeElement.getBoundingClientRect().x; const breakpoint = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint); - let maxWidth: string; - if (breakpoint?.maxWidth) { - if (this.isMobileSize(breakpoint)) { - maxWidth = `${breakpoint.maxWidth}px`; - } else { - maxWidth = `${breakpoint.maxWidth - xOffset}px`; + let maxWidth = '100%'; + let minWidth: string; + + if (breakpoint) { + const isMobile = this.isMobileSize(breakpoint); + if (breakpoint.maxWidth) { + maxWidth = isMobile ? `${breakpoint.maxWidth}px` : `${breakpoint.maxWidth - xOffset}px`; + } + if (breakpoint.minWidth) { + minWidth = isMobile ? `${breakpoint.minWidth}px` : `${breakpoint.minWidth - xOffset}px`; } - } else { - maxWidth = '100%'; } - const minWidth = breakpoint?.minWidth ? `${breakpoint.minWidth}px` : undefined; - this.mainLayoutSize.maxWidth = maxWidth; this.mainLayoutSize.minWidth = minWidth; } else { @@ -793,7 +794,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.mainLayoutSize.minWidth = undefined; } return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height || - prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth; + prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth || prevMainLayoutMinWidth !== this.mainLayoutSize.minWidth; } private updateRightLayoutSize(): boolean { From 779acf1891788d4a4bcd5c0e30118c1262ab6c03 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 28 Aug 2024 23:00:08 +0200 Subject: [PATCH 307/331] used sys tenant id for old protos --- .../subscription/DefaultTbLocalSubscriptionService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index ceafea6553..bd2ae08474 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -212,8 +212,13 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override public void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback) { + TenantId tenantId; + if (subEventCallback.getTenantIdMSB() == 0 && subEventCallback.getTenantIdLSB() == 0) { + tenantId = TenantId.SYS_TENANT_ID; //TODO: remove after release + } else { + tenantId = TenantId.fromUUID(new UUID(subEventCallback.getTenantIdMSB(), subEventCallback.getTenantIdLSB())); + } UUID entityId = new UUID(subEventCallback.getEntityIdMSB(), subEventCallback.getEntityIdLSB()); - TenantId tenantId = TenantId.fromUUID(new UUID(subEventCallback.getTenantIdMSB(), subEventCallback.getTenantIdLSB())); onSubEventCallback(tenantId, entityId, subEventCallback.getSeqNumber(), new TbEntityUpdatesInfo(subEventCallback.getAttributesUpdateTs(), subEventCallback.getTimeSeriesUpdateTs()), callback); } From 399c07259503c2fe5616b0c4d9a75e0cbe639941 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 28 Aug 2024 23:26:00 +0200 Subject: [PATCH 308/331] revert changes that are not related to this PR --- .../DefaultTbLocalSubscriptionService.java | 2 +- .../subscription/TbAlarmsSubscription.java | 8 ++++-- .../subscription/TbAttributeSubscription.java | 8 ++++-- .../subscription/TbEntityLocalSubsInfo.java | 28 +------------------ .../service/subscription/TbSubscription.java | 23 ++++++++++++--- .../subscription/TbSubscriptionUtils.java | 5 ++-- .../subscription/TbSubscriptionsInfo.java | 7 ++--- .../TbTimeSeriesSubscription.java | 8 ++++-- .../sub/AbstractNotificationSubscription.java | 5 ---- .../sub/NotificationsCountSubscription.java | 5 ---- .../sub/NotificationsSubscription.java | 5 ---- 11 files changed, 44 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index bd2ae08474..9e08e0359b 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -227,7 +227,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer onSubEventCallback(tenantId, entityId.getId(), seqNumber, entityUpdatesInfo, callback); } - public void onSubEventCallback(TenantId tenantId, UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { + private void onSubEventCallback(TenantId tenantId, UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { log.debug("[{}][{}][{}] Processing sub event callback: {}.", tenantId, entityId, seqNumber, entityUpdatesInfo); entityUpdates.put(entityId, entityUpdatesInfo); Set> pendingSubs = null; diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java index a9ab7797b7..a1d0aec29f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -36,8 +36,12 @@ public class TbAlarmsSubscription extends TbSubscription subscription : subs) { - if (subscription.getType() != sub.getType()) { - continue; // skip unchanged types - } switch (subscription.getType()) { case NOTIFICATIONS: case NOTIFICATIONS_COUNT: @@ -175,7 +151,6 @@ public class TbEntityLocalSubsInfo { var attrSub = (TbAttributeSubscription) subscription; if (!newState.attrAllKeys && attrSub.isAllKeys()) { newState.attrAllKeys = true; - newState.attrKeys = null; continue; } if (newState.attrKeys == null) { @@ -188,7 +163,6 @@ public class TbEntityLocalSubsInfo { var tsSub = (TbTimeSeriesSubscription) subscription; if (!newState.tsAllKeys && tsSub.isAllKeys()) { newState.tsAllKeys = true; - newState.tsKeys = null; continue; } if (newState.tsKeys == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index 68c042ea51..6d210befc0 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -15,25 +15,40 @@ */ package org.thingsboard.server.service.subscription; +import lombok.AllArgsConstructor; import lombok.Data; -import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Objects; import java.util.function.BiConsumer; @Data -@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY, doNotUseGetters = true) +@AllArgsConstructor public abstract class TbSubscription { - @EqualsAndHashCode.Exclude private final String serviceId; private final String sessionId; private final int subscriptionId; private final TenantId tenantId; private final EntityId entityId; private final TbSubscriptionType type; - @EqualsAndHashCode.Exclude private final BiConsumer, T> updateProcessor; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TbSubscription that = (TbSubscription) o; + return subscriptionId == that.subscriptionId && + sessionId.equals(that.sessionId) && + tenantId.equals(that.tenantId) && + entityId.equals(that.entityId) && + type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 3d0037caba..f5572a9a5c 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -109,9 +109,8 @@ public class TbSubscriptionUtils { .type(event); if (!ComponentLifecycleEvent.DELETED.equals(event)) { builder.info(new TbSubscriptionsInfo(proto.getNotifications(), proto.getAlarms(), - proto.getTsAllKeys(), proto.getAttrAllKeys(), - proto.getTsKeysCount() > 0 ? new HashSet<>(proto.getTsKeysList()) : null, - proto.getAttrKeysCount() > 0 ? new HashSet<>(proto.getAttrKeysList()) : null, + proto.getTsAllKeys(), proto.getTsKeysCount() > 0 ? new HashSet<>(proto.getTsKeysList()) : null, + proto.getAttrAllKeys(), proto.getAttrKeysCount() > 0 ? new HashSet<>(proto.getAttrKeysList()) : null, proto.getSeqNumber())); } return builder.build(); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java index 7430d2e386..48464d5297 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java @@ -27,17 +27,16 @@ import java.util.Set; */ @RequiredArgsConstructor @AllArgsConstructor -@EqualsAndHashCode +@EqualsAndHashCode(exclude = {"seqNumber"}) @ToString public class TbSubscriptionsInfo { protected boolean notifications; protected boolean alarms; protected boolean tsAllKeys; - protected boolean attrAllKeys; // primitives first for equals performance protected Set tsKeys; + protected boolean attrAllKeys; protected Set attrKeys; - @EqualsAndHashCode.Exclude protected int seqNumber; public boolean isEmpty() { @@ -49,7 +48,7 @@ public class TbSubscriptionsInfo { } protected TbSubscriptionsInfo copy(int seqNumber) { - return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, attrAllKeys, tsKeys, attrKeys, seqNumber); + return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys, attrAllKeys, attrKeys, seqNumber); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java index 0fc9a80122..10a24106ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java @@ -53,8 +53,12 @@ public class TbTimeSeriesSubscription extends TbSubscription extends TbSubscription super(serviceId, sessionId, subscriptionId, tenantId, entityId, type, updateProcessor); } - @Override - protected boolean canEqual(final Object other) { - return other instanceof AbstractNotificationSubscription; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java index 491acedb1e..171416a63d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java @@ -34,11 +34,6 @@ public class NotificationsCountSubscription extends AbstractNotificationSubscrip super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS_COUNT, updateProcessor); } - @Override - protected boolean canEqual(final Object other) { - return other instanceof NotificationsCountSubscription; - } - public UnreadNotificationsCountUpdate createUpdate() { return UnreadNotificationsCountUpdate.builder() .cmdId(getSubscriptionId()) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java index 867f48b72e..8306e7f95c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java @@ -47,11 +47,6 @@ public class NotificationsSubscription extends AbstractNotificationSubscription< this.limit = limit; } - @Override - protected boolean canEqual(final Object other) { - return other instanceof NotificationsSubscription; - } - public UnreadNotificationsUpdate createFullUpdate() { return UnreadNotificationsUpdate.builder() .cmdId(getSubscriptionId()) From 5602807dbe87978f68366f290ed8484599b8d5b8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak <50847617+AndriiLandiak@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:51:20 +0300 Subject: [PATCH 309/331] Introduce Edge queue to handle edge notification messages --- .../server/actors/app/AppActor.java | 21 -- .../device/DeviceActorMessageProcessor.java | 7 +- .../server/actors/tenant/TenantActor.java | 25 -- .../server/controller/EdgeController.java | 3 +- .../edge/DefaultEdgeNotificationService.java | 222 ------------ .../service/edge/EdgeContextComponent.java | 14 +- .../edge/EdgeEventSourcingListener.java | 3 +- ...DefaultEdgeInstallInstructionsService.java | 1 + ...DefaultEdgeUpgradeInstructionsService.java | 3 +- .../EdgeUpgradeInstructionsService.java | 1 + .../service/edge/rpc/EdgeGrpcService.java | 157 ++++---- .../service/edge/rpc/EdgeGrpcSession.java | 62 +++- .../service/edge/rpc/EdgeRpcService.java | 3 +- .../service/edge/rpc/EdgeSessionState.java | 1 + .../fetch/BasePageableEdgeEventFetcher.java | 2 +- .../rpc/fetch/RuleChainsEdgeEventFetcher.java | 2 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 9 +- .../rule/RuleChainEdgeProcessor.java | 2 +- .../rpc/sync/DefaultEdgeRequestsService.java | 6 +- .../entitiy/EntityStateSourcingListener.java | 18 +- .../entitiy/edge/DefaultTbEdgeService.java | 7 +- .../service/entitiy/edge/TbEdgeService.java | 2 + .../AbstractPartitionBasedService.java | 4 +- .../queue/DefaultTbClusterService.java | 123 +++++-- .../queue/DefaultTbCoreConsumerService.java | 37 +- .../queue/DefaultTbEdgeConsumerService.java | 334 ++++++++++++++++++ .../DefaultTbRuleEngineConsumerService.java | 2 +- .../service/queue/EdgeConsumerStats.java | 99 ++++++ .../service/queue/TbCoreConsumerStats.java | 31 +- .../service/queue/TbEdgeConsumerService.java | 23 ++ .../processing/AbstractConsumerService.java | 3 +- .../AbstractSubscriptionService.java | 2 +- .../src/main/resources/thingsboard.yml | 37 +- ...CaffeineCacheDefaultConfigurationTest.java | 6 +- .../controller/AbstractNotifyEntityTest.java | 26 +- .../server/controller/EdgeControllerTest.java | 57 ++- .../controller/EntityQueryControllerTest.java | 2 +- .../controller/TenantControllerTest.java | 14 +- .../server/edge/AbstractEdgeTest.java | 28 +- .../server/edge/DeviceEdgeTest.java | 67 +--- .../server/edge/TelemetryEdgeTest.java | 33 +- .../thingsboard/server/edge/UserEdgeTest.java | 15 +- .../discovery/HashPartitionServiceTest.java | 2 + .../notification/NotificationApiTest.java | 4 +- .../notification/NotificationRuleApiTest.java | 4 +- .../queue/DefaultTbClusterServiceTest.java | 7 +- .../service/script/TbelInvokeServiceTest.java | 4 +- .../ota/sql/OtaLwM2MIntegrationTest.java | 4 +- .../cache/RedisTbTransactionalCache.java | 5 +- .../cache/TbCaffeineCacheConfiguration.java | 4 +- .../server/cluster/TbClusterService.java | 20 +- .../server/dao/edge/EdgeService.java | 3 + .../server/common/data/CacheConstants.java | 1 + .../server/common/data/DataConstants.java | 2 + .../server/common/msg/MsgType.java | 5 +- .../common/msg/edge/EdgeEventUpdateMsg.java | 1 + .../common/msg/edge/EdgeHighPriorityMsg.java | 21 +- .../common/msg/edge/EdgeSessionMsg.java | 11 +- .../common/msg/edge/FromEdgeSyncResponse.java | 1 + .../common/msg/edge/ToEdgeSyncRequest.java | 2 + .../server/common/util/ProtoUtils.java | 59 +++- common/proto/src/main/proto/queue.proto | 43 ++- .../server/common/util/ProtoUtilsTest.java | 13 +- .../servicebus/TbServiceBusQueueConfigs.java | 7 + .../DefaultTbServiceInfoProvider.java | 2 +- .../queue/discovery/HashPartitionService.java | 46 +-- .../server/queue/discovery/QueueKey.java | 2 + .../server/queue/discovery/TopicService.java | 30 +- .../discovery/event/PartitionChangeEvent.java | 21 +- .../queue/kafka/TbKafkaTopicConfigs.java | 6 + .../provider/AwsSqsMonolithQueueFactory.java | 47 ++- .../provider/AwsSqsTbCoreQueueFactory.java | 42 ++- .../AwsSqsTbRuleEngineQueueFactory.java | 38 +- .../AwsSqsTbVersionControlQueueFactory.java | 1 - .../InMemoryMonolithQueueFactory.java | 27 +- .../provider/KafkaMonolithQueueFactory.java | 71 +++- .../provider/KafkaTbCoreQueueFactory.java | 55 ++- .../KafkaTbRuleEngineQueueFactory.java | 32 +- .../provider/PubSubMonolithQueueFactory.java | 45 ++- .../provider/PubSubTbCoreQueueFactory.java | 42 ++- .../PubSubTbRuleEngineQueueFactory.java | 20 +- .../RabbitMqMonolithQueueFactory.java | 48 ++- .../provider/RabbitMqTbCoreQueueFactory.java | 55 ++- .../RabbitMqTbRuleEngineQueueFactory.java | 23 +- .../RabbitMqTransportQueueFactory.java | 4 +- .../ServiceBusMonolithQueueFactory.java | 46 ++- .../ServiceBusTbCoreQueueFactory.java | 42 ++- .../ServiceBusTbRuleEngineQueueFactory.java | 23 +- .../ServiceBusTransportQueueFactory.java | 4 +- .../queue/provider/TbCoreQueueFactory.java | 10 + .../provider/TbCoreQueueProducerProvider.java | 17 + .../provider/TbQueueProducerProvider.java | 8 +- .../TbRuleEngineProducerProvider.java | 20 +- .../provider/TbRuleEngineQueueFactory.java | 11 +- .../TbTransportQueueProducerProvider.java | 16 +- .../TbVersionControlProducerProvider.java | 16 +- .../pubsub/TbPubSubSubscriptionSettings.java | 6 + .../rabbitmq/TbRabbitMqQueueArguments.java | 5 + .../queue/settings/TbQueueEdgeSettings.java | 31 ++ .../queue/sqs/TbAwsSqsQueueAttributes.java | 5 + .../server/common/stats/StatsType.java | 5 +- .../DefaultClusterVersionControlService.java | 3 +- .../server/dao/edge/EdgeServiceImpl.java | 64 ++-- .../dao/edge/EdgeSessionCaffeineCache.java | 33 ++ .../dao/edge/EdgeSessionRedisCache.java | 36 ++ .../resources/application-test.properties | 3 + 106 files changed, 2025 insertions(+), 773 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java rename application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java => common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java (55%) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 93cf5bd8c7..622812b327 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -37,12 +37,10 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -110,11 +108,6 @@ public class AppActor extends ContextAwareActor { case REMOVE_RPC_TO_DEVICE_ACTOR_MSG: onToDeviceActorMsg((TenantAwareMsg) msg, true); break; - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: - onToEdgeSessionMsg((EdgeSessionMsg) msg); - break; case SESSION_TIMEOUT_MSG: ctx.broadcastToChildrenByType(msg, EntityType.TENANT); break; @@ -206,20 +199,6 @@ public class AppActor extends ContextAwareActor { () -> true)); } - private void onToEdgeSessionMsg(EdgeSessionMsg msg) { - TbActorRef target = null; - if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) { - log.warn("Message has system tenant id: {}", msg); - } else { - target = getOrCreateTenantActor(msg.getTenantId()).orElse(null); - } - if (target != null) { - target.tellWithHighPriority(msg); - } else { - log.debug("[{}] Invalid edge session msg: {}", msg.getTenantId(), msg); - } - } - @Override public ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) { log.error("Failed to process msg: {}", msg, t); diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index b34ddca1af..3e8cd9592f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; @@ -214,7 +215,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId); try { if (systemContext.getEdgeService().isEdgeActiveAsync(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE).get()) { - saveRpcRequestToEdgeQueue(request, requestId).get(); + saveRpcRequestToEdgeQueue(request, requestId); } else { log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}. The Edge is currently offline or unreachable", tenantId, deviceId, edgeId.getId(), request); } @@ -921,7 +922,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getTbCoreToTransportService().process(nodeId, msg); } - private ListenableFuture saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { + private void saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { ObjectNode body = JacksonUtil.newObjectNode(); body.put("requestId", requestId); body.put("requestUUID", msg.getId().toString()); @@ -935,7 +936,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, body); - return systemContext.getEdgeEventService().saveAsync(edgeEvent); + systemContext.getClusterService().onEdgeHighPriorityMsg(new EdgeHighPriorityMsg(tenantId, edgeEvent)); } void restoreSessions() { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index d55e08143b..5edecba31a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -33,9 +33,7 @@ import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,14 +46,12 @@ import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; -import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; -import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; @@ -163,11 +159,6 @@ public class TenantActor extends RuleChainManagerActor { case RULE_CHAIN_TO_RULE_CHAIN_MSG: onRuleChainMsg((RuleChainAwareMsg) msg); break; - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: - onToEdgeSessionMsg((EdgeSessionMsg) msg); - break; default: return false; } @@ -270,18 +261,6 @@ public class TenantActor extends RuleChainManagerActor { initRuleChains(); } } - if (msg.getEntityId().getEntityType() == EntityType.EDGE && isCore) { - EdgeId edgeId = new EdgeId(msg.getEntityId().getId()); - EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService(); - if (edgeRpcService != null) { - if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { - edgeRpcService.deleteEdge(tenantId, edgeId); - } else if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) { - Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId); - edgeRpcService.updateEdge(tenantId, edge); - } - } - } if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent() && isMyPartition(msg.getEntityId())) { DeviceId deviceId = (DeviceId) msg.getEntityId(); onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); @@ -311,10 +290,6 @@ public class TenantActor extends RuleChainManagerActor { () -> true); } - private void onToEdgeSessionMsg(EdgeSessionMsg msg) { - systemContext.getEdgeRpcService().onToEdgeSessionMsg(tenantId, msg); - } - private ApiUsageState getApiUsageState() { if (apiUsageState == null) { apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId)); diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 26c30f85d1..2cb8eb7378 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -478,8 +478,7 @@ public class EdgeController extends BaseController { edgeId = checkNotNull(edgeId); SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); - ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId); - edgeRpcServiceOpt.get().processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); + edgeRpcServiceOpt.get().processSyncRequest(tenantId, edgeId, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java deleted file mode 100644 index 740b19e5d8..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 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.service.edge; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.customer.CustomerEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; - -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; - -@Service -@TbCoreComponent -@Slf4j -public class DefaultEdgeNotificationService implements EdgeNotificationService { - - public static final String EDGE_IS_ROOT_BODY_KEY = "isRoot"; - - @Autowired - private EdgeService edgeService; - - @Autowired - private EdgeProcessor edgeProcessor; - - @Autowired - private AssetEdgeProcessor assetProcessor; - - @Autowired - private AssetProfileEdgeProcessor assetProfileEdgeProcessor; - - @Autowired - private DeviceEdgeProcessor deviceProcessor; - - @Autowired - private DeviceProfileEdgeProcessor deviceProfileEdgeProcessor; - - @Autowired - private EntityViewEdgeProcessor entityViewProcessor; - - @Autowired - private DashboardEdgeProcessor dashboardProcessor; - - @Autowired - private RuleChainEdgeProcessor ruleChainProcessor; - - @Autowired - private UserEdgeProcessor userProcessor; - - @Autowired - private CustomerEdgeProcessor customerProcessor; - - @Autowired - private OtaPackageEdgeProcessor otaPackageProcessor; - - @Autowired - private WidgetBundleEdgeProcessor widgetBundleProcessor; - - @Autowired - private WidgetTypeEdgeProcessor widgetTypeProcessor; - - @Autowired - private QueueEdgeProcessor queueProcessor; - - @Autowired - private TenantEdgeProcessor tenantEdgeProcessor; - - @Autowired - private TenantProfileEdgeProcessor tenantProfileEdgeProcessor; - - @Autowired - private AlarmEdgeProcessor alarmProcessor; - - @Autowired - private RelationEdgeProcessor relationProcessor; - - @Autowired - private ResourceEdgeProcessor resourceEdgeProcessor; - - @Autowired - private NotificationEdgeProcessor notificationEdgeProcessor; - - @Autowired - private OAuth2EdgeProcessor oAuth2EdgeProcessor; - - @Autowired - protected ApplicationEventPublisher eventPublisher; - - @Value("${actors.system.edge_dispatcher_pool_size:4}") - private int edgeDispatcherSize; - - private ExecutorService executor; - - @PostConstruct - public void initExecutor() { - executor = ThingsBoardExecutors.newWorkStealingPool(edgeDispatcherSize, "edge-notifications"); - } - - @PreDestroy - public void shutdownExecutor() { - if (executor != null) { - executor.shutdownNow(); - } - } - - @Override - public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) { - edge.setRootRuleChainId(ruleChainId); - Edge savedEdge = edgeService.saveEdge(edge); - ObjectNode isRootBody = JacksonUtil.newObjectNode(); - isRootBody.put(EDGE_IS_ROOT_BODY_KEY, Boolean.TRUE); - eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edge.getId()).entityId(ruleChainId) - .body(JacksonUtil.toString(isRootBody)).actionType(ActionType.UPDATED).build()); - return savedEdge; - } - - @Override - public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) { - TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); - log.debug("[{}] Pushing notification to edge {}", tenantId, edgeNotificationMsg); - final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60); - try { - executor.submit(() -> { - try { - if (deadline < System.nanoTime()) { - log.warn("[{}] Skipping notification message because deadline reached {}", tenantId, edgeNotificationMsg); - return; - } - EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - switch (type) { - case EDGE -> edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); - case ASSET -> assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case ASSET_PROFILE -> assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DEVICE -> deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DEVICE_PROFILE -> deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case ENTITY_VIEW -> entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DASHBOARD -> dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case RULE_CHAIN -> ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case USER -> userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case CUSTOMER -> customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); - case OTA_PACKAGE -> otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case WIDGETS_BUNDLE -> widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case WIDGET_TYPE -> widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case QUEUE -> queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case ALARM -> alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); - case ALARM_COMMENT -> alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg); - case RELATION -> relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); - case TENANT -> tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case TENANT_PROFILE -> tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> - notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DOMAIN, OAUTH2_CLIENT -> oAuth2EdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); - } - } catch (Exception e) { - callBackFailure(tenantId, edgeNotificationMsg, callback, e); - } - }); - callback.onSuccess(); - } catch (Exception e) { - callBackFailure(tenantId, edgeNotificationMsg, callback, e); - } - } - - private void callBackFailure(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { - log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); - callback.onFailure(throwable); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 2d9fa01796..f0f41e3083 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -48,6 +48,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorFactory; @@ -97,6 +98,9 @@ public class EdgeContextComponent { @Autowired private EdgeService edgeService; + @Autowired(required = false) + private EdgeRpcService edgeRpcService; + @Autowired private EdgeEventService edgeEventService; @@ -224,19 +228,19 @@ public class EdgeContextComponent { private AdminSettingsEdgeProcessor adminSettingsProcessor; @Autowired - private OtaPackageEdgeProcessor otaPackageEdgeProcessor; + private OtaPackageEdgeProcessor otaPackageProcessor; @Autowired - private QueueEdgeProcessor queueEdgeProcessor; + private QueueEdgeProcessor queueProcessor; @Autowired - private TenantEdgeProcessor tenantEdgeProcessor; + private TenantEdgeProcessor tenantProcessor; @Autowired - private TenantProfileEdgeProcessor tenantProfileEdgeProcessor; + private TenantProfileEdgeProcessor tenantProfileProcessor; @Autowired - private ResourceEdgeProcessor resourceEdgeProcessor; + private ResourceEdgeProcessor resourceProcessor; @Autowired private NotificationEdgeProcessor notificationEdgeProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 789320e352..840750a781 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -130,8 +130,7 @@ public class EdgeEventSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) - && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) { + if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) { return; } try { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index 64f96aa4e4..7c8489067c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -94,4 +94,5 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad protected String getBaseDirName() { return INSTALL_DIR; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 275de6bafe..2f783c98ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -74,7 +74,8 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad Optional attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get(); if (attributeKvEntryOpt.isPresent()) { String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString()); - return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersion); + String appVersionFormatted = appVersion.replace("-SNAPSHOT", ""); + return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersionFormatted); } return false; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index 2517b615a8..47bde24bc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -31,4 +31,5 @@ public interface EdgeUpgradeInstructionsService { void setAppVersion(String version); boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception; + } 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 46bf3f69a4..5d45e1c3d5 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 @@ -28,14 +28,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -47,12 +50,14 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.state.DefaultDeviceStateService; @@ -84,7 +89,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); - private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); @Value("${edges.rpc.port}") @@ -111,7 +115,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Value("${edges.send_scheduler_pool_size}") private int sendSchedulerPoolSize; + @Value("${edges.max_high_priority_queue_size_per_session:10000}") + private int maxHighPriorityQueueSizePerSession; + @Autowired + @Lazy private EdgeContextComponent ctx; @Autowired @@ -120,6 +128,12 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Autowired private TbClusterService clusterService; + @Autowired + private TbServiceInfoProvider serviceInfoProvider; + + @Autowired + private TbTransactionalCache edgeIdServiceIdCache; + private Server server; private ScheduledExecutorService edgeEventProcessingExecutorService; @@ -188,79 +202,100 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Override public StreamObserver handleMsgs(StreamObserver outputStream) { - return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, this.maxInboundMessageSize).getInputStream(); + return new EdgeGrpcSession(ctx, + outputStream, + this::onEdgeConnect, + this::onEdgeDisconnect, + sendDownlinkExecutorService, + this.maxInboundMessageSize, + this.maxHighPriorityQueueSizePerSession).getInputStream(); } @Override public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) { - executorService.execute(() -> { - switch (msg.getMsgType()) { - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: - EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; - log.trace("[{}] onToEdgeSessionMsg [{}]", tenantId, msg); - onEdgeEvent(tenantId, edgeEventUpdateMsg.getEdgeId()); - break; - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: - ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; - log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); - startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId()); - break; - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: - FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; - log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); - processSyncResponse(fromEdgeSyncResponse); - break; + switch (msg.getMsgType()) { + case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> { + EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg; + log.trace("[{}] edgeEventMsg [{}]", tenantId, msg); + onEdgeHighPriorityEvent(edgeHighPriorityMsg); } - }); + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> { + EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; + log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg); + onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId()); + } + case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> { + ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; + log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); + startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId()); + } + case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> { + FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; + log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); + processSyncResponse(fromEdgeSyncResponse); + } + } } @Override public void updateEdge(TenantId tenantId, Edge edge) { - executorService.execute(() -> { - EdgeGrpcSession session = sessions.get(edge.getId()); - if (session != null && session.isConnected()) { - log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - session.onConfigurationUpdate(edge); - } else { - log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - } - }); + EdgeGrpcSession session = sessions.get(edge.getId()); + if (session != null && session.isConnected()) { + log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + session.onConfigurationUpdate(edge); + } else { + log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + } } @Override public void deleteEdge(TenantId tenantId, EdgeId edgeId) { - executorService.execute(() -> { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - session.close(); - sessions.remove(edgeId); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.remove(edgeId); - } finally { - newEventLock.unlock(); - } - cancelScheduleEdgeEventsCheck(edgeId); - } - }); - } - - private void onEdgeEvent(TenantId tenantId, EdgeId edgeId) { EdgeGrpcSession session = sessions.get(edgeId); if (session != null && session.isConnected()) { - log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId.getId()); + log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); + session.close(); + sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { - if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, true); - } + sessionNewEvents.remove(edgeId); } finally { newEventLock.unlock(); } + cancelScheduleEdgeEventsCheck(edgeId); + } + } + + private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId()); + updateSessionEventsFlag(tenantId, edgeId); + } + } + + private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) { + TenantId tenantId = msg.getTenantId(); + EdgeEvent edgeEvent = msg.getEdgeEvent(); + EdgeId edgeId = edgeEvent.getEdgeId(); + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId); + session.addEventToHighPriorityQueue(edgeEvent); + updateSessionEventsFlag(tenantId, edgeId); + } + } + + private void updateSessionEventsFlag(TenantId tenantId, EdgeId edgeId) { + final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); + newEventLock.lock(); + try { + if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) { + log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId()); + sessionNewEvents.put(edgeId, true); + } + } finally { + newEventLock.unlock(); } } @@ -279,37 +314,40 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); long lastConnectTs = System.currentTimeMillis(); save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); + edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId()); pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } - private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) { + private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { EdgeGrpcSession session = sessions.get(edgeId); if (session != null) { if (!session.isSyncCompleted()) { - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment")); + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); } else { boolean success = false; if (session.isConnected()) { session.startSyncProcess(true); success = true; } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, "")); + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId); } } } @Override - public void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer) { + public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) { + ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId()); + UUID requestId = request.getId(); EdgeGrpcSession session = sessions.get(request.getEdgeId()); if (session != null && !session.isSyncCompleted()) { responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); } else { - log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); + log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); localSyncEdgeRequests.put(requestId, responseConsumer); - clusterService.pushEdgeSyncRequestToCore(request); + clusterService.pushEdgeSyncRequestToEdge(request); scheduleSyncRequestTimeout(request, requestId); } } @@ -415,6 +453,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } else { log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); } + edgeIdServiceIdCache.evict(edgeId); } private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { 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 c4e8f1ec7c..c397807c5f 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 @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -96,6 +97,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -106,6 +108,7 @@ import java.util.function.BiConsumer; public final class EdgeGrpcSession implements Closeable { private static final ReentrantLock downlinkMsgLock = new ReentrantLock(); + private static final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected @@ -125,7 +128,7 @@ public final class EdgeGrpcSession implements Closeable { private StreamObserver inputStream; private StreamObserver outputStream; private boolean connected; - private boolean syncCompleted; + private volatile boolean syncCompleted; private Long newStartTs; private Long previousStartTs; @@ -137,11 +140,12 @@ public final class EdgeGrpcSession implements Closeable { private int maxInboundMessageSize; private int clientMaxInboundMessageSize; + private int maxHighPriorityQueueSizePerSession; private ScheduledExecutorService sendDownlinkExecutorService; EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { + BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { this.sessionId = UUID.randomUUID(); this.ctx = ctx; this.outputStream = outputStream; @@ -149,6 +153,7 @@ public final class EdgeGrpcSession implements Closeable { this.sessionCloseListener = sessionCloseListener; this.sendDownlinkExecutorService = sendDownlinkExecutorService; this.maxInboundMessageSize = maxInboundMessageSize; + this.maxHighPriorityQueueSizePerSession = maxHighPriorityQueueSizePerSession; initInputStream(); } @@ -223,7 +228,7 @@ public final class EdgeGrpcSession implements Closeable { } public void startSyncProcess(boolean fullSync) { - log.trace("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId); + log.info("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId); syncCompleted = false; interruptGeneralProcessingOnSync(); doSync(new EdgeSyncCursor(ctx, edge, fullSync)); @@ -255,18 +260,23 @@ public final class EdgeGrpcSession implements Closeable { Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { @Override public void onSuccess(Boolean isInterrupted) { - syncCompleted = true; - ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); + markSyncCompletedSendEdgeEventUpdate(); } @Override public void onFailure(Throwable t) { log.error("[{}][{}] Exception during sending sync complete", tenantId, edge.getId(), t); + markSyncCompletedSendEdgeEventUpdate(); } }, ctx.getGrpcCallbackExecutorService()); } } + private void markSyncCompletedSendEdgeEventUpdate() { + syncCompleted = true; + ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId())); + } + private void onUplinkMsg(UplinkMsg uplinkMsg) { if (isRateLimitViolated(uplinkMsg)) { return; @@ -429,6 +439,9 @@ public final class EdgeGrpcSession implements Closeable { private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { try { + if (!highPriorityQueue.isEmpty()) { + processHighPriorityEvents(); + } PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); if (isConnected() && !pageData.getData().isEmpty()) { log.trace("[{}][{}][{}] event(s) are going to be processed.", this.tenantId, this.sessionId, pageData.getData().size()); @@ -472,6 +485,20 @@ public final class EdgeGrpcSession implements Closeable { } } + private void processHighPriorityEvents() { + try { + List highPriorityEvents = new ArrayList<>(); + EdgeEvent event; + while ((event = highPriorityQueue.poll()) != null) { + highPriorityEvents.add(event); + } + List downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); + sendDownlinkMsgsPack(downlinkMsgsPack).get(); + } catch (Exception e) { + log.error("[{}] Failed to process high priority events", this.sessionId, e); + } + } + private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { interruptPreviousSendDownlinkMsgsTask(); @@ -556,7 +583,9 @@ public final class EdgeGrpcSession implements Closeable { DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { - case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, + CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, + ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { downlinkMsg = convertEntityEventToDownlink(edgeEvent); if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsgId()); @@ -683,15 +712,15 @@ public final class EdgeGrpcSession implements Closeable { case ADMIN_SETTINGS: return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent, this.edgeVersion); case OTA_PACKAGE: - return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getOtaPackageProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion); case TB_RESOURCE: - return ctx.getResourceEdgeProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getResourceProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion); case QUEUE: - return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getQueueProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion); case TENANT: - return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getTenantProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); case TENANT_PROFILE: - return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getTenantProfileProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); case NOTIFICATION_RULE: return ctx.getNotificationEdgeProcessor().convertNotificationRuleToDownlink(edgeEvent); case NOTIFICATION_TARGET: @@ -914,4 +943,15 @@ public final class EdgeGrpcSession implements Closeable { } } + public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { + while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) { + EdgeEvent oldestHighPriority = highPriorityQueue.poll(); + if (oldestHighPriority != null) { + log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}", + this.tenantId, edge.getId(), this.sessionId, oldestHighPriority); + } + } + highPriorityQueue.add(edgeEvent); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 12203db0c2..7c832ca760 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -32,5 +32,6 @@ public interface EdgeRpcService { void deleteEdge(TenantId tenantId, EdgeId edgeId); - void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer); + void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer); + } 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 6218b1f9a6..3f925c3787 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 @@ -30,4 +30,5 @@ public class EdgeSessionState { private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); private SettableFuture sendDownlinkMsgsFuture; private ScheduledFuture scheduledSendDownlinkTask; + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java index 747dc5722f..40fc3559ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java @@ -35,7 +35,7 @@ public abstract class BasePageableEdgeEventFetcher implements EdgeEventFetche @Override public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { - log.trace("[{}] start fetching edge events [{}]", tenantId, edge.getId()); + log.trace("[{}][{}] start fetching edge events [{}], pageLink {}", getClass().getSimpleName(), tenantId, edge.getId(), pageLink); PageData entities = fetchEntities(tenantId, edge, pageLink); List result = new ArrayList<>(); if (!entities.getData().isEmpty()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java index 5569c30018..328072aeea 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.rule.RuleChainService; -import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; +import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY; @Slf4j @AllArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 8dee74aac1..3c18e02324 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -374,23 +374,20 @@ public abstract class BaseEdgeProcessor { }, dbCallbackExecutorService); } - private boolean doSaveIfEdgeIsOffline(EdgeEventType type, - EdgeEventActionType action) { + private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) { return switch (action) { case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, UPDATED_COMMENT -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, - ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> - true; + ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; }; } private ListenableFuture doSaveEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventType type, EdgeEventActionType action, EntityId entityId, JsonNode body) { - log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], " + - "action [{}], entityId [{}], body [{}]", + log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], action [{}], entityId [{}], body [{}]", tenantId, edgeId, type, action, entityId, body); EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, type, action, entityId, body); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index b71493fa39..310d8d9b9b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -32,7 +32,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; +import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY; @Slf4j @Component diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 6e4117869b..46f1d7ef57 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -113,10 +113,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { if (ruleChainMetadataRequestMsg.getRuleChainIdMSB() == 0 || ruleChainMetadataRequestMsg.getRuleChainIdLSB() == 0) { return Futures.immediateFuture(null); } - RuleChainId ruleChainId = - new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB())); - return saveEdgeEvent(tenantId, edge.getId(), - EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null); + RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB())); + return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 08b1e57651..863be23e42 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -35,7 +35,6 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,6 +47,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; @@ -107,7 +108,7 @@ public class EntityStateSourcingListener { onDeviceProfileUpdate(deviceProfile, event.getOldEntity(), isCreated); } case EDGE -> { - handleEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent); + onEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent); } case TB_RESOURCE -> { TbResource tbResource = (TbResource) event.getEntity(); @@ -221,10 +222,7 @@ public class EntityStateSourcingListener { } private DeviceProfile getOldDeviceProfile(Object oldEntity) { - if (oldEntity instanceof DeviceProfile) { - return (DeviceProfile) oldEntity; - } - return null; + return oldEntity instanceof DeviceProfile ? (DeviceProfile) oldEntity : null; } private void onDeviceProfileDelete(TenantId tenantId, EntityId entityId, DeviceProfile deviceProfile) { @@ -241,11 +239,11 @@ public class EntityStateSourcingListener { tbClusterService.onDeviceUpdated(device, oldDevice); } - private void handleEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) { + private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) { if (entity instanceof Edge) { - tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); - } else if (entity instanceof EdgeEvent) { - tbClusterService.onEdgeEventUpdate(tenantId, (EdgeId) entityId); + tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent)); + } else if (entity instanceof EdgeEvent edgeEvent) { + tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index 9fcd8e2187..5b8fd949bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @AllArgsConstructor @@ -40,7 +39,6 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @Slf4j public class DefaultTbEdgeService extends AbstractTbEntityService implements TbEdgeService { - private final EdgeNotificationService edgeNotificationService; private final RuleChainService ruleChainService; @Override @@ -56,7 +54,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE if (ActionType.ADDED.equals(actionType)) { ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId); - edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId); } @@ -143,7 +141,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE TenantId tenantId = edge.getTenantId(); EdgeId edgeId = edge.getId(); try { - Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, ruleChainId); + Edge updatedEdge = edgeService.setEdgeRootRuleChain(tenantId, edge, ruleChainId); logEntityActionService.logEntityAction(tenantId, edgeId, edge, null, ActionType.UPDATED, user); return updatedEdge; } catch (Exception e) { @@ -152,4 +150,5 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE throw e; } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java index 8dc447b731..3afd32c3fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; public interface TbEdgeService { + Edge save(Edge edge, RuleChain edgeTemplateRootRuleChain, User user) throws Exception; void delete(Edge edge, User user); @@ -36,4 +37,5 @@ public interface TbEdgeService { Edge assignEdgeToPublicCustomer(TenantId tenantId, EdgeId edgeId, User user) throws ThingsboardException; Edge setEdgeRootRuleChain(Edge edge, RuleChainId ruleChainId, User user) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java index 676dbc1e3d..fc5ce5214c 100644 --- a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java +++ b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java @@ -90,13 +90,13 @@ public abstract class AbstractPartitionBasedService extends @Override protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { log.debug("onTbApplicationEvent, processing event: {}", partitionChangeEvent); - subscribeQueue.add(partitionChangeEvent.getPartitions()); + subscribeQueue.add(partitionChangeEvent.getCorePartitions()); scheduledExecutor.submit(this::pollInitStateFromDB); } @Override protected boolean filterTbApplicationEvent(PartitionChangeEvent event) { - return getServiceType().equals(event.getServiceType()); + return event.getServiceType() == getServiceType(); } protected void pollInitStateFromDB() { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 7699a4f7c2..301f8e8838 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -23,6 +23,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; @@ -55,6 +56,7 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -66,14 +68,23 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ComponentLifecycleMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.EntityDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ResourceDeleteMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ResourceUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; @@ -112,6 +123,8 @@ public class DefaultTbClusterService implements TbClusterService { private final AtomicInteger toRuleEngineMsgs = new AtomicInteger(0); private final AtomicInteger toRuleEngineNfs = new AtomicInteger(0); private final AtomicInteger toTransportNfs = new AtomicInteger(0); + private final AtomicInteger toEdgeMsgs = new AtomicInteger(0); + private final AtomicInteger toEdgeNfs = new AtomicInteger(0); @Autowired @Lazy @@ -133,6 +146,7 @@ public class DefaultTbClusterService implements TbClusterService { private final TbAssetProfileCache assetProfileCache; private final GatewayNotificationsService gatewayNotificationsService; private final EdgeService edgeService; + private final TbTransactionalCache edgeIdServiceIdCache; @Override public void pushMsgToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, TbQueueCallback callback) { @@ -169,7 +183,7 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void pushMsgToVersionControl(TenantId tenantId, TransportProtos.ToVersionControlServiceMsg msg, TbQueueCallback callback) { + public void pushMsgToVersionControl(TenantId tenantId, ToVersionControlServiceMsg msg, TbQueueCallback callback) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_VC_EXECUTOR, TenantId.SYS_TENANT_ID, tenantId); log.trace("PUSHING msg: {} to:{}", msg, tpi); producerProvider.getTbVersionControlMsgProducer().send(tpi, new TbProtoQueueMsg<>(tenantId.getId(), msg), callback); @@ -386,7 +400,7 @@ public class DefaultTbClusterService implements TbClusterService { if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { TenantId tenantId = resource.getTenantId(); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); - TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() + ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setResourceType(resource.getResourceType().name()) @@ -401,7 +415,7 @@ public class DefaultTbClusterService implements TbClusterService { public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) { if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { log.trace("[{}][{}][{}] Processing delete resource", resource.getTenantId(), resource.getResourceType(), resource.getResourceKey()); - TransportProtos.ResourceDeleteMsg resourceDeleteMsg = TransportProtos.ResourceDeleteMsg.newBuilder() + ResourceDeleteMsg resourceDeleteMsg = ResourceDeleteMsg.newBuilder() .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) .setResourceType(resource.getResourceType().name()) @@ -421,7 +435,7 @@ public class DefaultTbClusterService implements TbClusterService { private void broadcastEntityDeleteToTransport(TenantId tenantId, EntityId entityId, String name, TbQueueCallback callback) { log.trace("[{}][{}][{}] Processing [{}] delete event", tenantId, entityId.getEntityType(), entityId.getId(), name); - TransportProtos.EntityDeleteMsg entityDeleteMsg = TransportProtos.EntityDeleteMsg.newBuilder() + EntityDeleteMsg entityDeleteMsg = EntityDeleteMsg.newBuilder() .setEntityType(entityId.getEntityType().name()) .setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) @@ -453,39 +467,76 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { - log.trace("[{}] Processing edge {} event update ", tenantId, edgeId); - EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId); - ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdate(toProto(msg)).build(); - pushEdgeSyncMsgToCore(edgeId, toCoreMsg); + public void pushMsgToEdge(TenantId tenantId, EntityId entityId, ToEdgeMsg msg, TbQueueCallback callback) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, DataConstants.EDGE_QUEUE_NAME, tenantId, entityId); + TbQueueProducer> toEdgeProducer = producerProvider.getTbEdgeMsgProducer(); + toEdgeProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); + toEdgeMsgs.incrementAndGet(); } @Override - public void pushEdgeSyncRequestToCore(ToEdgeSyncRequest toEdgeSyncRequest) { - log.trace("[{}] Processing edge sync request {} ", toEdgeSyncRequest.getTenantId(), toEdgeSyncRequest); - ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToEdgeSyncRequest(toProto(toEdgeSyncRequest)).build(); - pushEdgeSyncMsgToCore(toEdgeSyncRequest.getEdgeId(), toCoreMsg); + public void onEdgeHighPriorityMsg(EdgeHighPriorityMsg msg) { + log.trace("[{}] Processing edge event for edgeId: {}", msg.getTenantId(), msg.getEdgeEvent().getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setEdgeHighPriority(toProto(msg)).build(); + processEdgeNotification(msg.getEdgeEvent().getEdgeId(), toEdgeNotificationMsg); } @Override - public void pushEdgeSyncResponseToCore(FromEdgeSyncResponse fromEdgeSyncResponse) { - log.trace("[{}] Processing edge sync response {}", fromEdgeSyncResponse.getTenantId(), fromEdgeSyncResponse); - ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setFromEdgeSyncResponse(toProto(fromEdgeSyncResponse)).build(); - pushEdgeSyncMsgToCore(fromEdgeSyncResponse.getEdgeId(), toCoreMsg); + public void onEdgeEventUpdate(EdgeEventUpdateMsg msg) { + log.trace("[{}] Processing edge event update for edgeId: {}", msg.getTenantId(), msg.getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setEdgeEventUpdate(toProto(msg)).build(); + processEdgeNotification(msg.getEdgeId(), toEdgeNotificationMsg); } - private void pushEdgeSyncMsgToCore(EdgeId edgeId, ToCoreNotificationMsg toCoreMsg) { - TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); - Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); - for (String serviceId : tbCoreServices) { - TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); - toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(edgeId.getId(), toCoreMsg), null); - toCoreNfs.incrementAndGet(); + @Override + public void onEdgeStateChangeEvent(ComponentLifecycleMsg msg) { + log.trace("[{}] Processing {} state change event: {}", msg.getTenantId(), EntityType.EDGE, msg.getEvent()); + ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setComponentLifecycle(componentLifecycleMsgProto).build(); + processEdgeNotification((EdgeId) msg.getEntityId(), toEdgeNotificationMsg); + } + + @Override + public void pushEdgeSyncRequestToEdge(ToEdgeSyncRequest request) { + log.trace("[{}] Processing edge sync request for edgeId: {}", request.getTenantId(), request.getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setToEdgeSyncRequest(toProto(request)).build(); + processEdgeNotification(request.getEdgeId(), toEdgeNotificationMsg); + } + + @Override + public void pushEdgeSyncResponseToCore(FromEdgeSyncResponse response, String requestServiceId) { + log.trace("[{}] Processing edge sync response for edgeId: {}", response.getTenantId(), response.getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setFromEdgeSyncResponse(toProto(response)).build(); + pushMsgToEdgeNotification(toEdgeNotificationMsg, requestServiceId); + } + + private void processEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { + var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); + serviceIdOpt.ifPresentOrElse( + serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), + () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) + ); + } + + private void pushMsgToEdgeNotification(ToEdgeNotificationMsg toEdgeNotificationMsg, String serviceId) { + TopicPartitionInfo tpi = topicService.getEdgeNotificationsTopic(serviceId); + TbQueueProducer> toEdgeNotificationProducer = producerProvider.getTbEdgeNotificationsMsgProducer(); + toEdgeNotificationProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), toEdgeNotificationMsg), null); + toEdgeNfs.incrementAndGet(); + } + + private void broadcastEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { + TbQueueProducer> toEdgeNotificationProducer = producerProvider.getTbEdgeNotificationsMsgProducer(); + Set serviceIds = partitionService.getAllServiceIds(ServiceType.TB_CORE); + for (String serviceId : serviceIds) { + TopicPartitionInfo tpi = topicService.getEdgeNotificationsTopic(serviceId); + toEdgeNotificationProducer.send(tpi, new TbProtoQueueMsg<>(edgeId.getId(), toEdgeNotificationMsg), null); + toEdgeNfs.incrementAndGet(); } } private void broadcast(ComponentLifecycleMsg msg) { - TransportProtos.ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); + ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); EntityType entityType = msg.getEntityId().getEntityType(); @@ -497,7 +548,6 @@ public class DefaultTbClusterService implements TbClusterService { || entityType.equals(EntityType.API_USAGE_STATE) || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) - || entityType.equals(EntityType.EDGE) || entityType.equals(EntityType.NOTIFICATION_RULE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); @@ -526,15 +576,17 @@ public class DefaultTbClusterService implements TbClusterService { int toRuleEngineMsgsCnt = toRuleEngineMsgs.getAndSet(0); int toRuleEngineNfsCnt = toRuleEngineNfs.getAndSet(0); int toTransportNfsCnt = toTransportNfs.getAndSet(0); - if (toCoreMsgCnt > 0 || toCoreNfsCnt > 0 || toRuleEngineMsgsCnt > 0 || toRuleEngineNfsCnt > 0 || toTransportNfsCnt > 0) { - log.info("To TbCore: [{}] messages [{}] notifications; To TbRuleEngine: [{}] messages [{}] notifications; To Transport: [{}] notifications", - toCoreMsgCnt, toCoreNfsCnt, toRuleEngineMsgsCnt, toRuleEngineNfsCnt, toTransportNfsCnt); + int toEdgeMsgCnt = toEdgeMsgs.getAndSet(0); + int toEdgeNfsCnt = toEdgeNfs.getAndSet(0); + if (toCoreMsgCnt > 0 || toCoreNfsCnt > 0 || toRuleEngineMsgsCnt > 0 || toRuleEngineNfsCnt > 0 || toTransportNfsCnt > 0 || toEdgeMsgCnt > 0 || toEdgeNfsCnt > 0) { + log.info("To TbCore: [{}] messages [{}] notifications; To TbRuleEngine: [{}] messages [{}] notifications; To Transport: [{}] notifications;" + + "To Edge: [{}] messages [{}] notifications", toCoreMsgCnt, toCoreNfsCnt, toRuleEngineMsgsCnt, toRuleEngineNfsCnt, toTransportNfsCnt, toEdgeMsgCnt, toEdgeNfsCnt); } } } private void sendDeviceStateServiceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { - TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); + DeviceStateServiceMsgProto.Builder builder = DeviceStateServiceMsgProto.newBuilder(); builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setDeviceIdMSB(deviceId.getId().getMostSignificantBits()); @@ -542,8 +594,8 @@ public class DefaultTbClusterService implements TbClusterService { builder.setAdded(added); builder.setUpdated(updated); builder.setDeleted(deleted); - TransportProtos.DeviceStateServiceMsgProto msg = builder.build(); - pushMsgToCore(tenantId, deviceId, TransportProtos.ToCoreMsg.newBuilder().setDeviceStateServiceMsg(msg).build(), null); + DeviceStateServiceMsgProto msg = builder.build(); + pushMsgToCore(tenantId, deviceId, ToCoreMsg.newBuilder().setDeviceStateServiceMsg(msg).build(), null); } @Override @@ -581,7 +633,7 @@ public class DefaultTbClusterService implements TbClusterService { return; } } - TransportProtos.EdgeNotificationMsgProto.Builder builder = TransportProtos.EdgeNotificationMsgProto.newBuilder(); + EdgeNotificationMsgProto.Builder builder = EdgeNotificationMsgProto.newBuilder(); builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setType(type.name()); @@ -602,9 +654,9 @@ public class DefaultTbClusterService implements TbClusterService { builder.setOriginatorEdgeIdMSB(originatorEdgeId.getId().getMostSignificantBits()); builder.setOriginatorEdgeIdLSB(originatorEdgeId.getId().getLeastSignificantBits()); } - TransportProtos.EdgeNotificationMsgProto msg = builder.build(); + EdgeNotificationMsgProto msg = builder.build(); log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg); - pushMsgToCore(tenantId, entityId != null ? entityId : tenantId, TransportProtos.ToCoreMsg.newBuilder().setEdgeNotificationMsg(msg).build(), null); + pushMsgToEdge(tenantId, entityId != null ? entityId : tenantId, ToEdgeMsg.newBuilder().setEdgeNotificationMsg(msg).build(), null); if (entityId != null && EntityType.DEVICE.equals(entityId.getEntityType())) { pushDeviceUpdateMessage(tenantId, edgeId, entityId, action); @@ -690,4 +742,5 @@ public class DefaultTbClusterService implements TbClusterService { toTransportNfs.incrementAndGet(); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 4462183baa..d30f03262d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -59,7 +59,6 @@ import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ErrorEventProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.LifecycleEventProto; @@ -86,7 +85,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -142,7 +140,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic())) .collect(Collectors.toSet())); } private void processMsgs(List> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception { - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); @@ -282,9 +277,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService 0) { partitionService.updateQueues(toCoreNotification.getQueueUpdateMsgsList()); callback.onSuccess(); @@ -686,13 +669,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbEdgeConsumerService { + + @Value("${queue.edge.pool-interval:25}") + private int pollInterval; + @Value("${queue.edge.pack-processing-timeout:10000}") + private int packProcessingTimeout; + @Value("${queue.core.consumer-per-partition:true}") + private boolean consumerPerPartition; + @Value("${queue.edge.pack-processing-retries:3}") + private int packProcessingRetries; + @Value("${queue.edge.stats.enabled:false}") + private boolean statsEnabled; + + private final TbCoreQueueFactory queueFactory; + private final EdgeContextComponent edgeCtx; + private final EdgeConsumerStats stats; + + private MainQueueConsumerManager, EdgeQueueConfig> mainConsumer; + + public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, + StatsFactory statsFactory, EdgeContextComponent edgeCtx) { + super(actorContext, null, null, null, null, null, + null, null); + this.edgeCtx = edgeCtx; + this.stats = new EdgeConsumerStats(statsFactory); + this.queueFactory = tbCoreQueueFactory; + } + + @PostConstruct + public void init() { + super.init("tb-edge"); + + this.mainConsumer = MainQueueConsumerManager., EdgeQueueConfig>builder() + .queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME)) + .config(EdgeQueueConfig.of(consumerPerPartition, (int) pollInterval)) + .msgPackProcessor(this::processMsgs) + .consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer()) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); + } + + @PreDestroy + public void destroy() { + super.destroy(); + } + + @Override + protected void startConsumers() { + super.startConsumers(); + } + + @Override + protected void onTbApplicationEvent(PartitionChangeEvent event) { + if (ServiceType.TB_CORE.equals(event.getServiceType())) { + var partitions = event.getEdgePartitions(); + log.info("Subscribing to partitions: {}", partitions); + mainConsumer.update(partitions); + } + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); + Future submitFuture = consumersExecutor.submit(() -> { + orderedMsgList.forEach((element) -> { + UUID id = element.getUuid(); + TbProtoQueueMsg msg = element.getMsg(); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + ToEdgeMsg toEdgeMsg = msg.getValue(); + pendingMsgHolder.setToEdgeMsg(toEdgeMsg); + if (toEdgeMsg.hasEdgeNotificationMsg()) { + pushNotificationToEdge(toEdgeMsg.getEdgeNotificationMsg(), 0, packProcessingRetries, callback); + } + if (statsEnabled) { + stats.log(toEdgeMsg); + } + } catch (Throwable e) { + log.warn("[{}] Failed to process message: {}", id, msg, e); + callback.onFailure(e); + } + }); + }); + if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { + if (!submitFuture.isDone()) { + submitFuture.cancel(true); + ToEdgeMsg lastSubmitMsg = pendingMsgHolder.getToEdgeMsg(); + log.info("Timeout to process message: {}", lastSubmitMsg); + } + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); + } + consumer.commit(); + } + + private static class PendingMsgHolder { + @Getter + @Setter + private volatile ToEdgeMsg toEdgeMsg; + } + + @Override + protected ServiceType getServiceType() { + return ServiceType.TB_CORE; + } + + @Override + protected long getNotificationPollDuration() { + return pollInterval; + } + + @Override + protected long getNotificationPackProcessingTimeout() { + return packProcessingTimeout; + } + + @Override + protected int getMgmtThreadPoolSize() { + return Math.max(Runtime.getRuntime().availableProcessors(), 4); + } + + @Override + protected TbQueueConsumer> createNotificationsConsumer() { + return queueFactory.createToEdgeNotificationsMsgConsumer(); + } + + @Override + protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { + ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue(); + try { + if (toEdgeNotificationMsg.hasEdgeHighPriority()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasComponentLifecycle()) { + ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle()); + TenantId tenantId = componentLifecycle.getTenantId(); + EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId()); + if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) { + edgeCtx.getEdgeRpcService().deleteEdge(tenantId, edgeId); + } else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) { + Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId); + edgeCtx.getEdgeRpcService().updateEdge(tenantId, edge); + } + callback.onSuccess(); + } + } catch (Exception e) { + log.error("Error processing edge notification message: {}", e.getMessage(), e); + callback.onFailure(e); + } + + if (statsEnabled) { + stats.log(msg.getValue()); + } + } + + private void pushNotificationToEdge(EdgeNotificationMsgProto edgeNotificationMsg, int retryCount, int retryLimit, TbCallback callback) { + TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); + log.debug("[{}] Pushing notification to edge {}", tenantId, edgeNotificationMsg); + try { + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + ListenableFuture future; + switch (type) { + case EDGE -> future = edgeCtx.getEdgeProcessor().processEdgeNotification(tenantId, edgeNotificationMsg); + case ASSET -> future = edgeCtx.getAssetProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case ASSET_PROFILE -> future = edgeCtx.getAssetProfileProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE -> future = edgeCtx.getDeviceProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE_PROFILE -> future = edgeCtx.getDeviceProfileProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case ENTITY_VIEW -> future = edgeCtx.getEntityViewProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DASHBOARD -> future = edgeCtx.getDashboardProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case RULE_CHAIN -> future = edgeCtx.getRuleChainProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case USER -> future = edgeCtx.getUserProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case CUSTOMER -> future = edgeCtx.getCustomerProcessor().processCustomerNotification(tenantId, edgeNotificationMsg); + case OTA_PACKAGE -> future = edgeCtx.getOtaPackageProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGETS_BUNDLE -> future = edgeCtx.getWidgetBundleProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGET_TYPE -> future = edgeCtx.getWidgetTypeProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case QUEUE -> future = edgeCtx.getQueueProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case ALARM -> future = edgeCtx.getAlarmProcessor().processAlarmNotification(tenantId, edgeNotificationMsg); + case ALARM_COMMENT -> future = edgeCtx.getAlarmProcessor().processAlarmCommentNotification(tenantId, edgeNotificationMsg); + case RELATION -> future = edgeCtx.getRelationProcessor().processRelationNotification(tenantId, edgeNotificationMsg); + case TENANT -> future = edgeCtx.getTenantProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case TENANT_PROFILE -> future = edgeCtx.getTenantProfileProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> + future = edgeCtx.getNotificationEdgeProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case TB_RESOURCE -> future = edgeCtx.getResourceProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DOMAIN, OAUTH2_CLIENT -> future = edgeCtx.getOAuth2EdgeProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + default -> { + future = Futures.immediateFuture(null); + log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); + } + } + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void unused) { + callback.onSuccess(); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (retryCount < retryLimit) { + log.warn("[{}] Retry {} for message due to failure: {}", tenantId, retryCount + 1, throwable.getMessage()); + pushNotificationToEdge(edgeNotificationMsg, retryCount + 1, retryLimit, callback); + } else { + callBackFailure(tenantId, edgeNotificationMsg, callback, throwable); + } + } + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + if (retryCount < retryLimit) { + log.warn("[{}] Retry {} for message due to exception: {}", tenantId, retryCount + 1, e.getMessage()); + pushNotificationToEdge(edgeNotificationMsg, retryCount + 1, retryLimit, callback); + } else { + callBackFailure(tenantId, edgeNotificationMsg, callback, e); + } + } + } + + private void callBackFailure(TenantId tenantId, EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { + log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); + callback.onFailure(throwable); + } + + @Scheduled(fixedDelayString = "${queue.edge.stats.print-interval-ms}") + public void printStats() { + if (statsEnabled) { + stats.printStats(); + stats.reset(); + } + } + + @Override + protected void stopConsumers() { + super.stopConsumers(); + mainConsumer.stop(); + mainConsumer.awaitStop(); + } + + @Data(staticConstructor = "of") + public static class EdgeQueueConfig implements QueueConfig { + private final boolean consumerPerPartition; + private final int pollInterval; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index d36d44e276..c9a6dee588 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -165,7 +165,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } @Override - protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception { + protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToRuleEngineNotificationMsg nfMsg = msg.getValue(); if (nfMsg.hasComponentLifecycle()) { handleComponentLifecycleMsg(id, ProtoUtils.fromProto(nfMsg.getComponentLifecycle())); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java new file mode 100644 index 0000000000..be2d060f03 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java @@ -0,0 +1,99 @@ +/** + * Copyright ยฉ 2016-2024 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.service.queue; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class EdgeConsumerStats { + + public static final String TOTAL_MSGS = "totalMsgs"; + public static final String EDGE_NOTIFICATIONS = "edgeNfs"; + public static final String TO_CORE_NF_EDGE_EVENT = "coreNfEdgeHPUpd"; + public static final String TO_CORE_NF_EDGE_EVENT_UPDATE = "coreNfEdgeUpd"; + public static final String TO_CORE_NF_EDGE_SYNC_REQUEST = "coreNfEdgeSyncReq"; + public static final String TO_CORE_NF_EDGE_SYNC_RESPONSE = "coreNfEdgeSyncResp"; + public static final String TO_CORE_NF_EDGE_COMPONENT_LIFECYCLE = "coreNfEdgeCompLfcl"; + + private final StatsCounter totalCounter; + private final StatsCounter edgeNotificationsCounter; + private final StatsCounter edgeHighPriorityCounter; + private final StatsCounter edgeEventUpdateCounter; + private final StatsCounter edgeSyncRequestCounter; + private final StatsCounter edgeSyncResponseCounter; + private final StatsCounter edgeComponentLifecycle; + + private final List counters = new ArrayList<>(7); + + public EdgeConsumerStats(StatsFactory statsFactory) { + String statsKey = StatsType.EDGE.getName(); + + this.totalCounter = register(statsFactory.createStatsCounter(statsKey, TOTAL_MSGS)); + this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); + this.edgeHighPriorityCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_EVENT)); + this.edgeEventUpdateCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_EVENT_UPDATE)); + this.edgeSyncRequestCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_REQUEST)); + this.edgeSyncResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_RESPONSE)); + this.edgeComponentLifecycle = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_COMPONENT_LIFECYCLE)); + } + + private StatsCounter register(StatsCounter counter) { + counters.add(counter); + return counter; + } + + public void log(ToEdgeNotificationMsg msg) { + totalCounter.increment(); + if (msg.hasEdgeHighPriority()) { + edgeHighPriorityCounter.increment(); + } else if (msg.hasEdgeEventUpdate()) { + edgeEventUpdateCounter.increment(); + } else if (msg.hasToEdgeSyncRequest()) { + edgeSyncRequestCounter.increment(); + } else if (msg.hasFromEdgeSyncResponse()) { + edgeSyncResponseCounter.increment(); + } else if (msg.hasComponentLifecycle()) { + edgeComponentLifecycle.increment(); + } + } + + public void log(ToEdgeMsg msg) { + totalCounter.increment(); + edgeNotificationsCounter.increment(); + } + + public void printStats() { + int total = totalCounter.get(); + if (total > 0) { + StringBuilder stats = new StringBuilder(); + counters.forEach(counter -> stats.append(counter.getName()).append(" = [").append(counter.get()).append("] ")); + log.info("Edge Stats: {}", stats); + } + } + + public void reset() { + counters.forEach(StatsCounter::clear); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java index 1bd1cb2d5e..aede1263eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java @@ -36,7 +36,6 @@ public class TbCoreConsumerStats { public static final String DEVICE_CLAIMS = "claimDevice"; public static final String DEVICE_STATES = "deviceState"; public static final String SUBSCRIPTION_MSGS = "subMsgs"; - public static final String EDGE_NOTIFICATIONS = "edgeNfs"; public static final String DEVICE_CONNECTS = "deviceConnect"; public static final String DEVICE_ACTIVITIES = "deviceActivity"; public static final String DEVICE_DISCONNECTS = "deviceDisconnect"; @@ -45,9 +44,6 @@ public class TbCoreConsumerStats { public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl"; public static final String TO_CORE_NF_DEVICE_RPC_RESPONSE = "coreNfDevRpcRsp"; - public static final String TO_CORE_NF_EDGE_EVENT_UPDATE = "coreNfEdgeUpd"; - public static final String TO_CORE_NF_EDGE_SYNC_REQUEST = "coreNfEdgeSyncReq"; - public static final String TO_CORE_NF_EDGE_SYNC_RESPONSE = "coreNfEdgeSyncResp"; public static final String TO_CORE_NF_NOTIFICATION_RULE_PROCESSOR = "coreNfNfRlProc"; public static final String TO_CORE_NF_QUEUE_UPDATE = "coreNfQueueUpd"; public static final String TO_CORE_NF_QUEUE_DELETE = "coreNfQueueDel"; @@ -65,7 +61,6 @@ public class TbCoreConsumerStats { private final StatsCounter claimDeviceCounter; private final StatsCounter deviceStateCounter; private final StatsCounter subscriptionMsgCounter; - private final StatsCounter edgeNotificationsCounter; private final StatsCounter deviceConnectsCounter; private final StatsCounter deviceActivitiesCounter; private final StatsCounter deviceDisconnectsCounter; @@ -74,9 +69,6 @@ public class TbCoreConsumerStats { private final StatsCounter toCoreNfOtherCounter; private final StatsCounter toCoreNfComponentLifecycleCounter; private final StatsCounter toCoreNfDeviceRpcResponseCounter; - private final StatsCounter toCoreNfEdgeEventUpdateCounter; - private final StatsCounter toCoreNfEdgeSyncRequestCounter; - private final StatsCounter toCoreNfEdgeSyncResponseCounter; private final StatsCounter toCoreNfNotificationRuleProcessorCounter; private final StatsCounter toCoreNfQueueUpdateCounter; private final StatsCounter toCoreNfQueueDeleteCounter; @@ -84,7 +76,7 @@ public class TbCoreConsumerStats { private final StatsCounter toCoreNfSubscriptionManagerCounter; private final StatsCounter toCoreNfVersionControlResponseCounter; - private final List counters = new ArrayList<>(24); + private final List counters = new ArrayList<>(23); public TbCoreConsumerStats(StatsFactory statsFactory) { String statsKey = StatsType.CORE.getName(); @@ -99,7 +91,6 @@ public class TbCoreConsumerStats { this.claimDeviceCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CLAIMS)); this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); - this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); this.deviceConnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CONNECTS)); this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES)); this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS)); @@ -109,9 +100,6 @@ public class TbCoreConsumerStats { this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER)); this.toCoreNfComponentLifecycleCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_COMPONENT_LIFECYCLE)); this.toCoreNfDeviceRpcResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_DEVICE_RPC_RESPONSE)); - this.toCoreNfEdgeEventUpdateCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_EVENT_UPDATE)); - this.toCoreNfEdgeSyncRequestCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_REQUEST)); - this.toCoreNfEdgeSyncResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_RESPONSE)); this.toCoreNfNotificationRuleProcessorCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_NOTIFICATION_RULE_PROCESSOR)); this.toCoreNfQueueUpdateCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_QUEUE_UPDATE)); this.toCoreNfQueueDeleteCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_QUEUE_DELETE)); @@ -155,11 +143,6 @@ public class TbCoreConsumerStats { deviceStateCounter.increment(); } - public void log(TransportProtos.EdgeNotificationMsgProto msg) { - totalCounter.increment(); - edgeNotificationsCounter.increment(); - } - public void log(TransportProtos.DeviceConnectProto msg) { totalCounter.increment(); deviceConnectsCounter.increment(); @@ -193,12 +176,6 @@ public class TbCoreConsumerStats { toCoreNfDeviceRpcResponseCounter.increment(); } else if (msg.hasComponentLifecycle()) { toCoreNfComponentLifecycleCounter.increment(); - } else if (msg.hasEdgeEventUpdate()) { - toCoreNfEdgeEventUpdateCounter.increment(); - } else if (msg.hasToEdgeSyncRequest()) { - toCoreNfEdgeSyncRequestCounter.increment(); - } else if (msg.hasFromEdgeSyncResponse()) { - toCoreNfEdgeSyncResponseCounter.increment(); } else if (msg.getQueueUpdateMsgsCount() > 0) { toCoreNfQueueUpdateCounter.increment(); } else if (msg.getQueueDeleteMsgsCount() > 0) { @@ -218,9 +195,8 @@ public class TbCoreConsumerStats { int total = totalCounter.get(); if (total > 0) { StringBuilder stats = new StringBuilder(); - counters.forEach(counter -> { - stats.append(counter.getName()).append(" = [").append(counter.get()).append("] "); - }); + counters.forEach(counter -> + stats.append(counter.getName()).append(" = [").append(counter.get()).append("] ")); log.info("Core Stats: {}", stats); } } @@ -228,4 +204,5 @@ public class TbCoreConsumerStats { public void reset() { counters.forEach(StatsCounter::clear); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java new file mode 100644 index 0000000000..6eaac782e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java @@ -0,0 +1,23 @@ +/** + * Copyright ยฉ 2016-2024 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.service.queue; + +import org.springframework.context.ApplicationListener; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; + +public interface TbEdgeConsumerService extends ApplicationListener { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index a264f912cb..1b7e5d53a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -124,7 +124,7 @@ public abstract class AbstractConsumerService> createNotificationsConsumer(); protected void processNotifications(List> msgs, TbQueueConsumer> consumer) throws Exception { - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); @@ -211,4 +211,5 @@ public abstract class AbstractConsumerService { softly.assertThat(name).as("cache name").isNotEmpty(); - softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0); + if (name.equals("edgeSessions")) { + softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isEqualTo(0); + } else { + softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0); + } softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0); }); softly.assertAll(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index d549407c44..238203cdb0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.model.ModelConstants; @@ -230,6 +231,19 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.reset(tbClusterService, auditLogService); } + protected void testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(HasName entity, EntityId entityId, EntityId originatorId, + TenantId tenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, int cntTimeBroadcast, Object... additionalInfo) { + int cntTime = 1; + testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType); + testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); + ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); + testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); + testBroadcastEdgeStateChangeEventTime(tenantId, cntTimeBroadcast); + Mockito.reset(tbClusterService, auditLogService); + } + + protected void testNotifyEntityBroadcastEntityStateChangeEventMany(HasName entity, HasName originator, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, @@ -351,11 +365,17 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { protected void testBroadcastEntityStateChangeEventTime(EntityId entityId, TenantId tenantId, int cntTime) { ArgumentMatcher matcherTenantIdId = cntTime > 1 || tenantId == null ? argument -> argument.getClass().equals(TenantId.class) : - argument -> argument.equals(tenantId) ; + argument -> argument.equals(tenantId); Mockito.verify(tbClusterService, times(cntTime)).broadcastEntityStateChangeEvent(Mockito.argThat(matcherTenantIdId), Mockito.any(entityId.getClass()), Mockito.any(ComponentLifecycleEvent.class)); } + protected void testBroadcastEdgeStateChangeEventTime(TenantId tenantId, int cntTime) { + ArgumentMatcher matcherTenantIdId = cntTime > 1 || tenantId == null ? argument -> argument.getClass().equals(TenantId.class) : + argument -> argument.equals(tenantId); + Mockito.verify(tbClusterService, times(cntTime)).onEdgeStateChangeEvent(Mockito.any(ComponentLifecycleMsg.class)); + } + private void testPushMsgToCoreTime(int cntTime) { Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull()); } @@ -608,8 +628,8 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { } private String entityClassToEntityTypeName(HasName entity) { - String entityType = entityClassToString(entity); - return "SAVE_OTA_PACKAGE_INFO_REQUEST".equals(entityType) || "OTA_PACKAGE_INFO".equals(entityType)? + String entityType = entityClassToString(entity); + return "SAVE_OTA_PACKAGE_INFO_REQUEST".equals(entityType) || "OTA_PACKAGE_INFO".equals(entityType) ? EntityType.OTA_PACKAGE.name().toUpperCase(Locale.ENGLISH) : entityType; } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index b636849774..4d38934dcb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -40,8 +40,8 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUpgradeInfo; @@ -62,6 +62,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -166,7 +167,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); Assert.assertEquals(edge.getName(), savedEdge.getName()); - testNotifyEntityBroadcastEntityStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, 2); @@ -176,7 +177,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); - testNotifyEntityBroadcastEntityStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.UPDATED, 1); } @@ -876,7 +877,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Asset asset = new Asset(); asset.setName("Test Sync Edge Asset 1"); - asset.setType("test"); + asset.setType("default"); Asset savedAsset = doPost("/api/asset", asset, Asset.class); Device device = new Device(); @@ -896,7 +897,7 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); - edgeImitator.expectMessageAmount(27); + edgeImitator.expectMessageAmount(26); edgeImitator.connect(); waitForMessages(edgeImitator); @@ -905,16 +906,16 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); - Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); - Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + printQueueMsgsIfNotEmpty(edgeImitator); - edgeImitator.expectMessageAmount(22); - doPost("/api/edge/sync/" + edge.getId()); + edgeImitator.expectMessageAmount(21); + doPost("/api/edge/sync/" + edge.getId()).andExpect(status().isOk()); waitForMessages(edgeImitator); verifyFetchersMsgs(edgeImitator, savedDevice); - Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + printQueueMsgsIfNotEmpty(edgeImitator); edgeImitator.allowIgnoredTypes(); try { @@ -930,6 +931,15 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + private static void printQueueMsgsIfNotEmpty(EdgeImitator edgeImitator) { + if (!edgeImitator.getDownlinkMsgs().isEmpty()) { + for (AbstractMessage downlinkMsg : edgeImitator.getDownlinkMsgs()) { + log.warn("Unexpected message in the queue: {}", downlinkMsg); + } + } + Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + } + private RuleChainId getEdgeRootRuleChainId(EdgeImitator edgeImitator) { try { EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB())); @@ -948,11 +958,18 @@ public class EdgeControllerTest extends AbstractControllerTest { } private void simulateEdgeActivation(Edge edge) throws Exception { + Awaitility.await() + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List ruleChains = getEdgeRuleChains(edge.getId()); + return ruleChains.size() == 1 && ruleChains.get(0).getId().equals(edge.getRootRuleChainId()); + }); + ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); - doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); + doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + AttributeScope.SERVER_SCOPE, attributes); Awaitility.await() - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); @@ -960,8 +977,9 @@ public class EdgeControllerTest extends AbstractControllerTest { if (activeAttrOpt.isEmpty()) { return false; } + List ruleChains = getEdgeRuleChains(edge.getId()); Map activeAttr = activeAttrOpt.get(); - return "true".equals(activeAttr.get("value").toString()); + return "true".equals(activeAttr.get("value").toString()) && ruleChains.size() == 1; }); } @@ -988,13 +1006,12 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); - Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popUserMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, TENANT_ADMIN_EMAIL, Authority.TENANT_ADMIN)); Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); - Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId)); Assert.assertTrue(popTenantProfileMsg(edgeImitator.getDownlinkMsgs(), tenantProfileId)); @@ -1223,6 +1240,12 @@ public class EdgeControllerTest extends AbstractControllerTest { return doPost("/api/edge", edge, Edge.class); } + private List getEdgeRuleChains(EdgeId edgeId) throws Exception { + return doGetTypedWithTimePageLink("/api/edge/" + edgeId + "/ruleChains?", + new TypeReference>() { + }, new TimePageLink(10)).getData(); + } + @Test public void testGetEdgeInstallInstructions() throws Exception { Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy"); @@ -1271,6 +1294,8 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.1.5"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); + edgeUpgradeInstructionsService.setAppVersion("3.6.1.6-SNAPSHOT"); + Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.2"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); @@ -1281,6 +1306,8 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.2"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); + edgeUpgradeInstructionsService.setAppVersion("3.6.2-SNAPSHOT"); + Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.2.6"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 17de555bdc..5d04b16dfa 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -629,7 +629,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Awaitility.await() .alias("data by query") - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); var loadedEntities = new ArrayList<>(data.getData()); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index e1beda28d8..c8cc72c5b5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -664,7 +664,7 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); return !tpi.getTenantId().get().isSysTenantId(); @@ -678,7 +678,7 @@ public class TenantControllerTest extends AbstractControllerTest { tenantProfile.setIsolatedTbRuleEngine(false); tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) .getTenantId().get().isSysTenantId()); @@ -690,7 +690,7 @@ public class TenantControllerTest extends AbstractControllerTest { submittedMsgs.add(tbMsg.getId()); Thread.sleep(timeLeft / msgs); } - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); }); @@ -719,13 +719,13 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull(); }); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId); assertThat(tpi.getTenantId()).hasValue(tenantId); TbMsg tbMsg = publishTbMsg(tenantId, tpi); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); })); @@ -733,7 +733,7 @@ public class TenantControllerTest extends AbstractControllerTest { deleteDifferentTenant(); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNull(); assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId)) .isInstanceOf(TenantNotFoundException.class); @@ -753,7 +753,7 @@ public class TenantControllerTest extends AbstractControllerTest { } private void verifyUsedQueueAndMessage(String queue, TenantId tenantId, EntityId entityId, String msgType, Runnable action, Consumer tpiAssert) { - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId); tpiAssert.accept(tpi); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index bd8a59a975..b3a1dd790e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edge; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; @@ -27,7 +28,6 @@ import org.junit.Before; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -127,9 +127,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { @Autowired protected EdgeEventService edgeEventService; - @Autowired - protected TbClusterService clusterService; - @Before public void setupEdgeTest() throws Exception { loginSysAdmin(); @@ -207,7 +204,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { + "/asset/" + savedAsset.getUuidId(), Asset.class); // wait until assign device and asset events are fully processed by edge notification service - TimeUnit.MILLISECONDS.sleep(500); + TimeUnit.MILLISECONDS.sleep(1000); } protected void extendDeviceProfileData(DeviceProfile deviceProfile) { @@ -697,4 +694,25 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { .andExpect(status().isOk()); } + + protected ObjectNode createDefaultRpc() { + return createDefaultRpc(1); + } + + protected ObjectNode createDefaultRpc(Integer value) { + ObjectNode rpc = JacksonUtil.newObjectNode(); + rpc.put("method", "setGpio"); + + ObjectNode params = JacksonUtil.newObjectNode(); + + params.put("pin", 7); + params.put("value", value); + + rpc.set("params", params); + rpc.put("persistent", true); + rpc.put("timeout", 5000); + + return rpc; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 071d6b1391..387011717a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -51,7 +51,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -79,9 +78,13 @@ import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; @TestPropertySource(properties = { "transport.mqtt.enabled=true" @@ -107,7 +110,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); @@ -184,7 +187,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); @@ -399,7 +402,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder(); - deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + deviceDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE); deviceDeleteMsgBuilder.setIdMSB(savedDevice.getId().getId().getMostSignificantBits()); deviceDeleteMsgBuilder.setIdLSB(savedDevice.getId().getId().getLeastSignificantBits()); testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder); @@ -624,17 +627,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { if (keyValueProto.getKey().equals(expectedKey)) { Assert.assertEquals(expectedKey, keyValueProto.getKey()); switch (keyValueProto.getType()) { - case STRING_V: - Assert.assertEquals(expectedValue, keyValueProto.getStringV()); - break; - case LONG_V: - Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV()); - break; - case JSON_V: - Assert.assertEquals(JacksonUtil.toJsonNode(expectedValue), JacksonUtil.toJsonNode(keyValueProto.getJsonV())); - break; - default: - Assert.fail("Unexpected data type: " + keyValueProto.getType()); + case STRING_V -> Assert.assertEquals(expectedValue, keyValueProto.getStringV()); + case LONG_V -> Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV()); + case JSON_V -> Assert.assertEquals(JacksonUtil.toJsonNode(expectedValue), JacksonUtil.toJsonNode(keyValueProto.getJsonV())); + default -> Assert.fail("Unexpected data type: " + keyValueProto.getType()); } found = true; } @@ -768,9 +764,6 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(1, relatedEdgeIds.size()); Assert.assertEquals(tmpEdge.getId(), relatedEdgeIds.get(0)); - // clean up stored edge events - edgeEventService.cleanupEvents(1); - // edge is disconnected: perform rpc call - no edge event saved doPostAsync( "/api/rpc/oneway/" + device.getId().getId().toString(), @@ -779,10 +772,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { status().isOk()); Awaitility.await() .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - PageData result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1)); - return result.getTotalElements() == 0; - }); + .untilAsserted(() -> verify(tbClusterService, never()).onEdgeHighPriorityMsg(any())); // edge is connected: perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly simulateEdgeActivation(tmpEdge); @@ -792,21 +782,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest { String.class, status().isOk()); - final AtomicReference> resultRef = new AtomicReference<>(); Awaitility.await() .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - PageData result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1)); - resultRef.set(result); - return result != null && result.getData().size() == 1; - }); - - PageData result = resultRef.get(); - EdgeEvent edgeEvent = result.getData().get(0); - Assert.assertEquals(EdgeEventActionType.RPC_CALL, edgeEvent.getAction()); - Assert.assertEquals(EdgeEventType.DEVICE, edgeEvent.getType()); - Assert.assertEquals(tmpEdge.getId(), edgeEvent.getEdgeId()); - Assert.assertEquals(device.getId().getId(), edgeEvent.getEntityId()); + .untilAsserted(() -> verify(tbClusterService, times(1)).onEdgeHighPriorityMsg(any())); // clean up tmp edge doDelete("/api/edge/" + tmpEdge.getId().getId().toString()).andExpect(status().isOk()); @@ -824,27 +802,12 @@ public class DeviceEdgeTest extends AbstractEdgeTest { private DeviceCredentials buildDeviceCredentialsForUplinkMsg(DeviceId deviceId) { DeviceCredentials deviceCredentials = new DeviceCredentials(); deviceCredentials.setDeviceId(deviceId); + deviceCredentials.setCredentialsId(String.valueOf(UUID.randomUUID())); deviceCredentials.setCredentialsValue("NEW_TOKEN"); deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); return deviceCredentials; } - private ObjectNode createDefaultRpc() { - ObjectNode rpc = JacksonUtil.newObjectNode(); - rpc.put("method", "setGpio"); - - ObjectNode params = JacksonUtil.newObjectNode(); - - params.put("pin", 7); - params.put("value", 1); - - rpc.set("params", params); - rpc.put("persistent", true); - rpc.put("timeout", 5000); - - return rpc; - } - private void simulateEdgeActivation(Edge edge) throws Exception { ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index b77e0b5ccc..2afe17f935 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -33,33 +33,46 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.edge.v1.RpcRequestMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.List; import java.util.concurrent.TimeUnit; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @DaoSqlTest public class TelemetryEdgeTest extends AbstractEdgeTest { @Test public void testTimeseriesWithFailures() throws Exception { int numberOfTimeseriesToSend = 333; + int numberOfHighPriorityRpcRequests = 13; + int frequencyOfRpcRequestsPerTimeseries = 25; Device device = findDeviceByName("Edge Device 1"); edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); // imitator will generate failure in 5% of cases edgeImitator.setFailureProbability(5.0); - edgeImitator.expectMessageAmount(numberOfTimeseriesToSend); + edgeImitator.expectMessageAmount(numberOfTimeseriesToSend + numberOfHighPriorityRpcRequests); for (int idx = 1; idx <= numberOfTimeseriesToSend; idx++) { String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); edgeEventService.saveAsync(edgeEvent).get(); + if (idx % frequencyOfRpcRequestsPerTimeseries == 0) { + doPostAsync( + "/api/rpc/oneway/" + device.getId().getId().toString(), + JacksonUtil.toString(createDefaultRpc(idx)), + String.class, + status().isOk()); + } } Assert.assertTrue(edgeImitator.waitForMessages(120)); @@ -71,6 +84,14 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { Assert.assertTrue(isIdxExistsInTheDownlinkList(idx, allTelemetryMsgs)); } + List allDeviceRpcCallMsgs = edgeImitator.findAllMessagesByType(DeviceRpcCallMsg.class); + Assert.assertEquals(numberOfHighPriorityRpcRequests, allDeviceRpcCallMsgs.size()); + + for (int idx = 1; idx <= numberOfHighPriorityRpcRequests; idx++) { + int pinValue = idx * frequencyOfRpcRequestsPerTimeseries; + Assert.assertTrue(isPinValueExistsInTheRpcDownlinkList(pinValue, allDeviceRpcCallMsgs)); + } + edgeImitator.setRandomFailuresOnTimeseriesDownlink(false); } @@ -175,6 +196,16 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { return false; } + private boolean isPinValueExistsInTheRpcDownlinkList(int idx, List rpcDownlinkMsgs) { + for (DeviceRpcCallMsg proto : rpcDownlinkMsgs) { + RpcRequestMsg rpcRequestMsg = proto.getRequestMsg(); + if ((JacksonUtil.toJsonNode(rpcRequestMsg.getParams())).get("value").intValue() == idx) { + return true; + } + } + return false; + } + @Test public void testTimeseriesDeliveryFailuresForever_deliverOnlyDeviceUpdateMsgs() throws Exception { int numberOfMsgsToSend = 100; diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index fc9eeaa36e..1c3b5ccb7a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -46,10 +45,9 @@ public class UserEdgeTest extends AbstractEdgeTest { private BCryptPasswordEncoder passwordEncoder; @Test - @Ignore("Ignored until fix") public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(4); + edgeImitator.expectMessageAmount(6); User newTenantAdmin = new User(); newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); newTenantAdmin.setTenantId(tenantId); @@ -57,7 +55,9 @@ public class UserEdgeTest extends AbstractEdgeTest { newTenantAdmin.setFirstName("Boris"); newTenantAdmin.setLastName("Johnson"); User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 4 messages - x2 user update msg and x2 user credentials update msgs + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); @@ -133,7 +133,7 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); // create user - edgeImitator.expectMessageAmount(4); + edgeImitator.expectMessageAmount(6); User customerUser = new User(); customerUser.setAuthority(Authority.CUSTOMER_USER); customerUser.setTenantId(tenantId); @@ -142,7 +142,9 @@ public class UserEdgeTest extends AbstractEdgeTest { customerUser.setFirstName("John"); customerUser.setLastName("Edwards"); User savedCustomerUser = createUser(customerUser, "customer"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 4 messages - x2 user update msg and x2 user credentials update msgs + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); @@ -253,4 +255,5 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } + } diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index c468387c55..52cbaa4add 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -429,6 +429,8 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(partitionService, "vcTopic", "tb.vc"); ReflectionTestUtils.setField(partitionService, "vcPartitions", 10); ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName); + ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge"); + ReflectionTestUtils.setField(partitionService, "edgePartitions", 10); partitionService.init(); partitionService.partitionsInit(); return partitionService; diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 9981b7935d..53ae5f36ae 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -904,7 +904,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { String expectedSubject = "Comment on 'test' alarm"; String expectedBody = TENANT_ADMIN_EMAIL + " added comment: text"; ArgumentCaptor> msgCaptor = ArgumentCaptor.forClass(Map.class); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), eq(TEST_MOBILE_TOKEN), eq(expectedSubject), eq(expectedBody), @@ -959,7 +959,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { } private NotificationRequestStats awaitNotificationRequest(NotificationRequestId requestId) { - return await().atMost(30, TimeUnit.SECONDS) + return await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> getStats(requestId), Objects::nonNull); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 9cf73894c2..852ddd757c 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -780,7 +780,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(getMyNotifications(false, 100)).size().isZero(); createDevice("Device 1", "default", "111"); - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(getMyNotifications(false, 100)).size().isEqualTo(1); }); @@ -798,7 +798,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRulesCache.evict(tenantId); createDevice("Device 3", "default", "333"); - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(getMyNotifications(false, 100)).size().isEqualTo(2); }); diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 59cca779f5..25fe589a08 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -25,6 +25,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -98,6 +100,8 @@ public class DefaultTbClusterServiceTest { protected TbQueueProducerProvider producerProvider; @MockBean protected TbRuleEngineProducerService ruleEngineProducerService; + @MockBean + protected TbTransactionalCache edgeCache; @SpyBean protected TopicService topicService; @@ -400,4 +404,5 @@ public class DefaultTbClusterServiceTest { ((DefaultTbClusterService) clusterService).getRuleEngineProfileForEntityOrElseNull(tenantId, assetId, tbMsg); verify(assetProfileCache, times(1)).get(tenantId, assetProfileId); } -} \ No newline at end of file + +} diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index 76f8ff654e..9e3b6221b1 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -69,14 +69,14 @@ class TbelInvokeServiceTest extends AbstractControllerTest { for (int i = 0; i < 100; i++) { msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); Assert.assertEquals(expected, result); } long startTs = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); Assert.assertEquals(expected, result); } long duration = System.currentTimeMillis() - startTs; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 4f24c58916..b78e738d72 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -95,7 +95,7 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED); List ts = await("await on timeseries") - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/values/timeseries?orderBy=ASC&keys=fw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { @@ -127,7 +127,7 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); List ts = await("await on timeseries") - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> getSwStateTelemetryFromAPI(device.getId().getId()), this::predicateForStatuses); log.warn("Object9: Got the ts: {}", ts); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index 3531c3846e..fbf88bb5e4 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -72,9 +72,10 @@ public abstract class RedisTbTransactionalCache x.get(cacheName)) + .map(specs -> specs.get(cacheName)) .map(CacheSpecs::getTimeToLiveInMinutes) - .map(t -> Expiration.from(t, TimeUnit.MINUTES)) + .filter(ttl -> !ttl.equals(0)) + .map(ttl -> Expiration.from(ttl, TimeUnit.MINUTES)) .orElseGet(Expiration::persistent); this.cacheEnabled = Optional.ofNullable(cacheSpecsMap) .map(CacheSpecsMap::getSpecs) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index fe47a7a02d..be3a264720 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -75,8 +75,10 @@ public class TbCaffeineCacheConfiguration { = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) .maximumWeight(cacheSpec.getMaxSize()) - .expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES) .ticker(ticker()); + if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { + caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); + } return new CaffeineCache(name, caffeineBuilder.build()); } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 399a27f07e..5112e93da7 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -29,13 +29,17 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; @@ -52,7 +56,7 @@ public interface TbClusterService extends TbQueueClusterService { void pushMsgToCore(ToDeviceActorNotificationMsg msg, TbQueueCallback callback); - void broadcastToCore(TransportProtos.ToCoreNotificationMsg msg); + void broadcastToCore(ToCoreNotificationMsg msg); void pushMsgToVersionControl(TenantId tenantId, ToVersionControlServiceMsg msg, TbQueueCallback callback); @@ -96,11 +100,17 @@ public interface TbClusterService extends TbQueueClusterService { void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback); - void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId); + void onEdgeHighPriorityMsg(EdgeHighPriorityMsg msg); - void pushEdgeSyncRequestToCore(ToEdgeSyncRequest toEdgeSyncRequest); + void onEdgeEventUpdate(EdgeEventUpdateMsg msg); - void pushEdgeSyncResponseToCore(FromEdgeSyncResponse fromEdgeSyncResponse); + void onEdgeStateChangeEvent(ComponentLifecycleMsg msg); + + void pushEdgeSyncRequestToEdge(ToEdgeSyncRequest request); + + void pushEdgeSyncResponseToCore(FromEdgeSyncResponse response, String requestServiceId); + + void pushMsgToEdge(TenantId tenantId, EntityId entityId, ToEdgeMsg msg, TbQueueCallback callback); void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java index 278232d2f3..c18f42ace7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -97,6 +98,8 @@ public interface EdgeService extends EntityDaoService { String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName); + Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws Exception; + ListenableFuture isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String activityState); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index adf626a8d0..54a5bc755d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -25,6 +25,7 @@ public class CacheConstants { public static final String USER_CACHE = "users"; public static final String ENTITY_VIEW_CACHE = "entityViews"; public static final String EDGE_CACHE = "edges"; + public static final String EDGE_SESSIONS_CACHE = "edgeSessions"; public static final String RELATED_EDGES_CACHE = "relatedEdges"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 9641b5e69f..aef3751f0c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -140,4 +140,6 @@ public class DataConstants { public static final String SQ_QUEUE_TOPIC = "tb_rule_engine.sq"; public static final String QUEUE_NAME = "queueName"; + public static final String EDGE_QUEUE_NAME = "Edge"; + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 68f2db88b2..b05bb75032 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.msg; import lombok.Getter; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -36,7 +37,7 @@ public enum MsgType { /** * ADDED/UPDATED/DELETED events for main entities. * - * See {@link org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg} + * See {@link ComponentLifecycleMsg} */ COMPONENT_LIFE_CYCLE_MSG, @@ -126,6 +127,7 @@ public enum MsgType { * Message that is sent on Edge Event to Edge Session */ EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG, + EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG, /** * Messages that are sent to and from edge session to start edge synchronization process @@ -143,4 +145,5 @@ public enum MsgType { MsgType(boolean ignoreOnStart) { this.ignoreOnStart = ignoreOnStart; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java index 3fd70f90c7..892b2a57dd 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java @@ -32,4 +32,5 @@ public class EdgeEventUpdateMsg implements EdgeSessionMsg { public MsgType getMsgType() { return MsgType.EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java similarity index 55% rename from application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java index 4f90023f68..03f039f2e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java @@ -13,17 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge; +package org.thingsboard.server.common.msg.edge; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.id.RuleChainId; +import lombok.Data; +import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.common.msg.MsgType; -public interface EdgeNotificationService { +@Data +public class EdgeHighPriorityMsg implements EdgeSessionMsg { - Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws Exception; + private final TenantId tenantId; + private final EdgeEvent edgeEvent; + + @Override + public MsgType getMsgType() { + return MsgType.EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG; + } - void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java index 58e31234fc..4e658d6dd4 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java @@ -15,8 +15,13 @@ */ package org.thingsboard.server.common.msg.edge; -import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; + +public interface EdgeSessionMsg { + + TenantId getTenantId(); + + MsgType getMsgType(); -public interface EdgeSessionMsg extends TenantAwareMsg, ToAllNodesMsg { } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java index baf483310c..1e8beea443 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java @@ -37,4 +37,5 @@ public class FromEdgeSyncResponse implements EdgeSessionMsg { public MsgType getMsgType() { return MsgType.EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java index 6c9ac545b4..04516b58c5 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java @@ -30,9 +30,11 @@ public class ToEdgeSyncRequest implements EdgeSessionMsg { private final UUID id; private final TenantId tenantId; private final EdgeId edgeId; + private final String serviceId; @Override public MsgType getMsgType() { return MsgType.EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG; } + } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 5646a37a68..b61dc16384 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.util; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; @@ -36,6 +38,8 @@ import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfigu import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -68,6 +72,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -140,6 +145,7 @@ public class ProtoUtils { .setRequestIdLSB(request.getId().getLeastSignificantBits()) .setEdgeIdMSB(request.getEdgeId().getId().getMostSignificantBits()) .setEdgeIdLSB(request.getEdgeId().getId().getLeastSignificantBits()) + .setServiceId(request.getServiceId()) .build(); } @@ -147,7 +153,8 @@ public class ProtoUtils { return new ToEdgeSyncRequest( new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), - new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) + EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), + proto.getServiceId() ); } @@ -168,12 +175,56 @@ public class ProtoUtils { return new FromEdgeSyncResponse( new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), - new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), + EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), proto.getSuccess(), proto.getError() ); } + public static TransportProtos.EdgeHighPriorityMsgProto toProto(EdgeHighPriorityMsg msg) { + TransportProtos.EdgeHighPriorityMsgProto.Builder builder = TransportProtos.EdgeHighPriorityMsgProto.newBuilder() + .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getTenantId().getId().getLeastSignificantBits()) + .setType(msg.getEdgeEvent().getType().name()) + .setAction(msg.getEdgeEvent().getAction().name()); + + if (msg.getEdgeEvent().getEntityId() != null) { + builder.setEntityIdMSB(msg.getEdgeEvent().getEntityId().getMostSignificantBits()); + builder.setEntityIdLSB(msg.getEdgeEvent().getEntityId().getLeastSignificantBits()); + } + if (msg.getEdgeEvent().getEdgeId() != null) { + builder.setEdgeIdMSB(msg.getEdgeEvent().getEdgeId().getId().getMostSignificantBits()); + builder.setEdgeIdLSB(msg.getEdgeEvent().getEdgeId().getId().getLeastSignificantBits()); + } + if (msg.getEdgeEvent().getBody() != null) { + builder.setBody(JacksonUtil.toString(msg.getEdgeEvent().getBody())); + } + + return builder.build(); + } + + public static EdgeHighPriorityMsg fromProto(TransportProtos.EdgeHighPriorityMsgProto proto) { + EdgeEventType type = EdgeEventType.valueOf(proto.getType()); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(proto.getAction()); + JsonNode body = proto.hasBody() ? JacksonUtil.toJsonNode(proto.getBody()) : null; + + EdgeId edgeId = null; + if (proto.hasEdgeIdMSB() && proto.hasEdgeIdLSB()) { + edgeId = EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())); + } + + EntityId entityId = null; + if (proto.hasEntityIdMSB() && proto.hasEntityIdLSB()) { + entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); + } + + return new EdgeHighPriorityMsg( + TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), + EdgeUtils.constructEdgeEvent(TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), + edgeId, type, actionType, entityId, body) + ); + } + public static TransportProtos.EdgeEventUpdateMsgProto toProto(EdgeEventUpdateMsg msg) { return TransportProtos.EdgeEventUpdateMsgProto.newBuilder() .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) @@ -186,7 +237,7 @@ public class ProtoUtils { public static EdgeEventUpdateMsg fromProto(TransportProtos.EdgeEventUpdateMsgProto proto) { return new EdgeEventUpdateMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), - new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) + EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) ); } @@ -208,7 +259,7 @@ public class ProtoUtils { private static DeviceEdgeUpdateMsg fromProto(TransportProtos.DeviceEdgeUpdateMsgProto proto) { EdgeId edgeId = null; if (proto.hasEdgeIdMSB() && proto.hasEdgeIdLSB()) { - edgeId = new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())); + edgeId = EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())); } return new DeviceEdgeUpdateMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 74f1d0560f..ac29ff4d0b 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -146,9 +146,9 @@ message KeyValueProto { } enum AttributeScopeProto { - CLIENT_SCOPE = 0; - SERVER_SCOPE = 1; - SHARED_SCOPE = 2; + CLIENT_SCOPE = 0; + SERVER_SCOPE = 1; + SHARED_SCOPE = 2; } message AttributeKey { @@ -421,7 +421,7 @@ message GatewayDisconnectDeviceMsg { } enum TransportApiRequestErrorCode { - UNKNOWN_TRANSPORT_API_ERROR = 0; + UNKNOWN_TRANSPORT_API_ERROR = 0; ENTITY_LIMIT = 1; } @@ -1140,6 +1140,18 @@ message EdgeNotificationMsgProto { int64 originatorEdgeIdLSB = 14; } +message EdgeHighPriorityMsgProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + optional int64 edgeIdMSB = 3; + optional int64 edgeIdLSB = 4; + string type = 5; + string action = 6; + optional string body = 7; + optional int64 entityIdMSB = 8; + optional int64 entityIdLSB = 9; +} + message EdgeEventUpdateMsgProto { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; @@ -1154,6 +1166,7 @@ message ToEdgeSyncRequestMsgProto { int64 requestIdLSB = 4; int64 edgeIdMSB = 5; int64 edgeIdLSB = 6; + string serviceId = 7; } message FromEdgeSyncResponseMsgProto { @@ -1475,7 +1488,7 @@ message ToCoreMsg { DeviceStateServiceMsgProto deviceStateServiceMsg = 2; SubscriptionMgrMsgProto toSubscriptionMgrMsg = 3; bytes toDeviceActorNotificationMsg = 4 [deprecated = true]; - EdgeNotificationMsgProto edgeNotificationMsg = 5; + EdgeNotificationMsgProto edgeNotificationMsg = 5 [deprecated = true]; DeviceActivityProto deviceActivityMsg = 6; NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7; LifecycleEventProto lifecycleEventMsg = 8; @@ -1498,13 +1511,27 @@ message ToCoreNotificationMsg { NotificationRuleProcessorMsg notificationRuleProcessorMsg = 7; ComponentLifecycleMsgProto componentLifecycle = 8; CoreStartupMsg coreStartupMsg = 9; - EdgeEventUpdateMsgProto edgeEventUpdate = 10; - ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 11; - FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 12; + EdgeEventUpdateMsgProto edgeEventUpdate = 10 [deprecated = true]; + ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 11 [deprecated = true]; + FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 12 [deprecated = true]; ResourceCacheInvalidateMsg resourceCacheInvalidateMsg = 13; RestApiCallResponseMsgProto restApiCallResponseMsg = 50; } + +/* Messages to Edge queue that are handled by ThingsBoard Core Service */ +message ToEdgeMsg { + EdgeNotificationMsgProto edgeNotificationMsg = 1; +} + +message ToEdgeNotificationMsg { + EdgeHighPriorityMsgProto edgeHighPriority = 1; + EdgeEventUpdateMsgProto edgeEventUpdate = 2; + ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 3; + FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 4; + ComponentLifecycleMsgProto componentLifecycle = 5; +} + /* Messages that are handled by ThingsBoard RuleEngine Service */ message ToRuleEngineMsg { int64 tenantIdMSB = 1; diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index a513d3e311..b253b4e444 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; @@ -34,6 +35,8 @@ import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConf import org.thingsboard.server.common.data.device.data.DeviceConfiguration; import org.thingsboard.server.common.data.device.data.DeviceTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; @@ -54,6 +57,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -110,6 +114,13 @@ class ProtoUtilsTest { } } + @Test + void protoEdgeHighPrioritySerialization() { + EdgeHighPriorityMsg msg = new EdgeHighPriorityMsg(tenantId, EdgeUtils.constructEdgeEvent(tenantId, edgeId, + EdgeEventType.DEVICE, EdgeEventActionType.ADDED, deviceId, JacksonUtil.newObjectNode())); + assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); + } + @Test void protoEdgeEventUpdateSerialization() { EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId); @@ -118,7 +129,7 @@ class ProtoUtilsTest { @Test void protoToEdgeSyncRequestSerialization() { - ToEdgeSyncRequest msg = new ToEdgeSyncRequest(id, tenantId, edgeId); + ToEdgeSyncRequest msg = new ToEdgeSyncRequest(id, tenantId, edgeId, "serviceId"); assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java index 45d39d1cd6..80da7846ea 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java @@ -27,6 +27,7 @@ import java.util.Map; @Component @ConditionalOnExpression("'${queue.type:null}'=='service-bus'") public class TbServiceBusQueueConfigs { + @Value("${queue.service-bus.queue-properties.core:}") private String coreProperties; @Value("${queue.service-bus.queue-properties.rule-engine:}") @@ -39,6 +40,9 @@ public class TbServiceBusQueueConfigs { private String jsExecutorProperties; @Value("${queue.service-bus.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.service-bus.queue-properties.edge:}") + private String edgeProperties; + @Getter private Map coreConfigs; @Getter @@ -51,6 +55,8 @@ public class TbServiceBusQueueConfigs { private Map jsExecutorConfigs; @Getter private Map vcConfigs; + @Getter + private Map edgeConfigs; @PostConstruct private void init() { @@ -60,6 +66,7 @@ public class TbServiceBusQueueConfigs { notificationsConfigs = PropertyUtils.getProps(notificationsProperties); jsExecutorConfigs = PropertyUtils.getProps(jsExecutorProperties); vcConfigs = PropertyUtils.getProps(vcProperties); + edgeConfigs = PropertyUtils.getProps(edgeProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 4caa3b92b3..1ceb52b3d6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -88,7 +88,7 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { assignedTenantProfiles = Collections.emptySet(); } - generateNewServiceInfoWithCurrentSystemInfo(); + generateNewServiceInfoWithCurrentSystemInfo(); } @AfterContextReady diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 53a2152782..37e519e3f2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -51,6 +51,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.EDGE_QUEUE_NAME; import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @Service @@ -59,12 +60,16 @@ public class HashPartitionService implements PartitionService { @Value("${queue.core.topic}") private String coreTopic; - @Value("${queue.core.partitions:100}") + @Value("${queue.core.partitions:10}") private Integer corePartitions; @Value("${queue.vc.topic:tb_version_control}") private String vcTopic; @Value("${queue.vc.partitions:10}") private Integer vcPartitions; + @Value("${queue.edge.topic:tb_edge}") + private String edgeTopic; + @Value("${queue.edge.partitions:10}") + private Integer edgePartitions; @Value("${queue.partitions.hash_function_name:murmur3_128}") private String hashFunctionName; @@ -114,6 +119,10 @@ public class HashPartitionService implements PartitionService { if (!isTransport(serviceInfoProvider.getServiceType())) { doInitRuleEnginePartitions(); } + + QueueKey edgeKey = coreKey.withQueueName(EDGE_QUEUE_NAME); + partitionSizesMap.put(edgeKey, edgePartitions); + partitionTopicsMap.put(edgeKey, edgeTopic); } @AfterStartUp(order = AfterStartUp.QUEUE_INFO_INITIALIZATION) @@ -195,8 +204,7 @@ public class HashPartitionService implements PartitionService { .map(queueDeleteMsg -> { TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); return new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); - }) - .collect(Collectors.toList()); + }).toList(); queueKeys.forEach(queueKey -> { removeQueue(queueKey); evictTenantInfo(queueKey.getTenantId()); @@ -210,8 +218,7 @@ public class HashPartitionService implements PartitionService { @Override public void removeTenant(TenantId tenantId) { List queueKeys = partitionSizesMap.keySet().stream() - .filter(queueKey -> tenantId.equals(queueKey.getTenantId())) - .collect(Collectors.toList()); + .filter(queueKey -> tenantId.equals(queueKey.getTenantId())).toList(); queueKeys.forEach(this::removeQueue); evictTenantInfo(tenantId); } @@ -546,12 +553,10 @@ public class HashPartitionService implements PartitionService { if (routingInfo == null) { throw new TenantNotFoundException(tenantId); } - switch (serviceType) { - case TB_RULE_ENGINE: - return routingInfo.isIsolated(); - default: - return false; + if (serviceType == ServiceType.TB_RULE_ENGINE) { + return routingInfo.isIsolated(); } + return false; } private TenantRoutingInfo getRoutingInfo(TenantId tenantId) { @@ -588,7 +593,10 @@ public class HashPartitionService implements PartitionService { responsibleServices.computeIfAbsent(profileId, k -> new ArrayList<>()).add(instance); } } - } else if (ServiceType.TB_CORE.equals(serviceType) || ServiceType.TB_VC_EXECUTOR.equals(serviceType)) { + } else if (ServiceType.TB_CORE.equals(serviceType)) { + queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); + queueServiceList.computeIfAbsent(new QueueKey(serviceType).withQueueName(EDGE_QUEUE_NAME), key -> new ArrayList<>()).add(instance); + } else if (ServiceType.TB_VC_EXECUTOR.equals(serviceType)) { queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); } } @@ -649,16 +657,12 @@ public class HashPartitionService implements PartitionService { } public static HashFunction forName(String name) { - switch (name) { - case "murmur3_32": - return Hashing.murmur3_32(); - case "murmur3_128": - return Hashing.murmur3_128(); - case "sha256": - return Hashing.sha256(); - default: - throw new IllegalArgumentException("Can't find hash function with name " + name); - } + return switch (name) { + case "murmur3_32" -> Hashing.murmur3_32(); + case "murmur3_128" -> Hashing.murmur3_128(); + case "sha256" -> Hashing.sha256(); + default -> throw new IllegalArgumentException("Can't find hash function with name " + name); + }; } private List toServiceIds(Collection serviceInfos) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java index f0ae5af43c..b991e4614c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.With; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; public class QueueKey { private final ServiceType type; + @With private final String queueName; private final TenantId tenantId; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 960be231c6..d8a409e704 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -30,8 +30,9 @@ public class TopicService { @Value("${queue.prefix:}") private String prefix; - private Map tbCoreNotificationTopics = new HashMap<>(); - private Map tbRuleEngineNotificationTopics = new HashMap<>(); + private final Map tbCoreNotificationTopics = new HashMap<>(); + private final Map tbRuleEngineNotificationTopics = new HashMap<>(); + private final Map tbEdgeNotificationTopics = new HashMap<>(); /** * Each Service should start a consumer for messages that target individual service instance based on serviceId. @@ -41,16 +42,21 @@ public class TopicService { * @return */ public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) { - switch (serviceType) { - case TB_CORE: - return tbCoreNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - case TB_RULE_ENGINE: - return tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - default: - return buildNotificationsTopicPartitionInfo(serviceType, serviceId); - } + return switch (serviceType) { + case TB_CORE -> tbCoreNotificationTopics.computeIfAbsent(serviceId, + id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + case TB_RULE_ENGINE -> tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, + id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + default -> buildNotificationsTopicPartitionInfo(serviceType, serviceId); + }; + } + + public TopicPartitionInfo getEdgeNotificationsTopic(String serviceId) { + return tbEdgeNotificationTopics.computeIfAbsent(serviceId, id -> buildEdgeNotificationsTopicPartitionInfo(serviceId)); + } + + private TopicPartitionInfo buildEdgeNotificationsTopicPartitionInfo(String serviceId) { + return buildTopicPartitionInfo("tb_edge.notifications." + serviceId, null, null, false); } private TopicPartitionInfo buildNotificationsTopicPartitionInfo(ServiceType serviceType, String serviceId) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index b5ad181a00..88ceb4aa08 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -17,17 +17,20 @@ package org.thingsboard.server.queue.discovery.event; import lombok.Getter; import lombok.ToString; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.discovery.QueueKey; -import java.util.Collections; +import java.io.Serial; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; @ToString(callSuper = true) public class PartitionChangeEvent extends TbApplicationEvent { + @Serial private static final long serialVersionUID = -8731788167026510559L; @Getter @@ -41,9 +44,19 @@ public class PartitionChangeEvent extends TbApplicationEvent { this.partitionsMap = partitionsMap; } - // only for service types that have single QueueKey - public Set getPartitions() { - return partitionsMap.values().stream().findAny().orElse(Collections.emptySet()); + public Set getCorePartitions() { + return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_CORE, DataConstants.MAIN_QUEUE_NAME); } + public Set getEdgePartitions() { + return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_CORE, DataConstants.EDGE_QUEUE_NAME); + } + + private Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { + return partitionsMap.entrySet() + .stream() + .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toSet()); + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index 6f237894c2..300548e081 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -27,6 +27,7 @@ import java.util.Map; @Component @ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") public class TbKafkaTopicConfigs { + public static final String NUM_PARTITIONS_SETTING = "partitions"; @Value("${queue.kafka.topic-properties.core:}") @@ -43,6 +44,8 @@ public class TbKafkaTopicConfigs { private String fwUpdatesProperties; @Value("${queue.kafka.topic-properties.version-control:}") private String vcProperties; + @Value("${queue.kafka.topic-properties.edge:}") + private String edgeProperties; @Value("${queue.kafka.topic-properties.housekeeper:}") private String housekeeperProperties; @Value("${queue.kafka.topic-properties.housekeeper-reprocessing:}") @@ -70,6 +73,8 @@ public class TbKafkaTopicConfigs { private Map housekeeperConfigs; @Getter private Map housekeeperReprocessingConfigs; + @Getter + private Map edgeConfigs; @PostConstruct private void init() { @@ -86,6 +91,7 @@ public class TbKafkaTopicConfigs { vcConfigs = PropertyUtils.getProps(vcProperties); housekeeperConfigs = PropertyUtils.getProps(housekeeperProperties); housekeeperReprocessingConfigs = PropertyUtils.getProps(housekeeperReprocessingProperties); + edgeConfigs = PropertyUtils.getProps(edgeProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index d40a8bac85..005e9aee17 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -24,15 +24,17 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -45,6 +47,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -70,6 +73,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbAwsSqsSettings sqsSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueAdmin coreAdmin; @@ -79,6 +83,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin otaAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public AwsSqsMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -87,6 +92,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng TbQueueTransportNotificationSettings transportNotificationSettings, TbAwsSqsSettings sqsSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, TbQueueRemoteJsInvokeSettings jsInvokeSettings) { this.topicService = topicService; @@ -97,6 +103,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.transportNotificationSettings = transportNotificationSettings; this.sqsSettings = sqsSettings; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.jsInvokeSettings = jsInvokeSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); @@ -106,6 +113,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); this.otaAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getOtaAttributes()); this.vcAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getVcAttributes()); + this.edgeAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getEdgeAttributes()); } @Override @@ -130,13 +138,14 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -223,11 +232,10 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbAwsSqsProducerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic())); } - @Override public TbQueueProducer> createHousekeeperMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @@ -249,6 +257,30 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbAwsSqsProducerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -272,5 +304,8 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index 05e435d7fb..515c1bba44 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -22,15 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -43,6 +45,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -69,6 +72,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +81,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin otaAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public AwsSqsTbCoreQueueFactory(TbAwsSqsSettings sqsSettings, TbQueueCoreSettings coreSettings, @@ -84,6 +89,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueRuleEngineSettings ruleEngineSettings, TopicService topicService, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, @@ -92,6 +98,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; this.ruleEngineSettings = ruleEngineSettings; + this.edgeSettings = edgeSettings; this.topicService = topicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; @@ -105,6 +112,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); this.otaAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getOtaAttributes()); this.vcAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getVcAttributes()); + this.edgeAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getEdgeAttributes()); } @Override @@ -129,7 +137,8 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -202,7 +211,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbAwsSqsProducerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -228,6 +237,30 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbAwsSqsProducerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -251,5 +284,8 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index 7e6129a748..37319e378b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -25,8 +25,14 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -37,6 +43,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -59,12 +66,14 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbAwsSqsSettings sqsSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin otaAdmin; + private final TbQueueAdmin edgeAdmin; public AwsSqsTbRuleEngineQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -72,7 +81,8 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory TbAwsSqsSettings sqsSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbQueueTransportNotificationSettings transportNotificationSettings) { + TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings) { this.topicService = topicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -80,12 +90,14 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory this.sqsSettings = sqsSettings; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); this.ruleEngineAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getRuleEngineAttributes()); this.jsExecutorAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getJsExecutorAttributes()); this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); this.otaAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getOtaAttributes()); + this.edgeAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getEdgeAttributes()); } @Override @@ -99,7 +111,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(ruleEngineSettings.getTopic())); } @@ -109,10 +121,20 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createTbCoreNotificationsMsgProducer() { + public TbQueueProducer> createTbCoreNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbAwsSqsProducerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, topicService.buildTopicName(configuration.getTopic()), @@ -120,10 +142,10 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { + public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, topicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -150,17 +172,17 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(otaAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java index b14bb8d8e7..67ff0393bd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java @@ -41,7 +41,6 @@ public class AwsSqsTbVersionControlQueueFactory implements TbVersionControlQueue private final TbQueueVersionControlSettings vcSettings; private final TopicService topicService; - private final TbQueueAdmin coreAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 225290ed63..c4281bc390 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -34,6 +34,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -51,6 +52,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueVersionControlSettings vcSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final InMemoryStorage storage; public InMemoryMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, @@ -59,6 +61,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE TbServiceInfoProvider serviceInfoProvider, TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, InMemoryStorage storage) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -67,6 +70,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.ruleEngineSettings = ruleEngineSettings; this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.storage = storage; } @@ -92,7 +96,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getTopic())); + return new InMemoryTbQueueProducer<>(storage, topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -180,10 +184,29 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") private void printInMemoryStats() { storage.printStats(); } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 87be03c8c7..46d94f094f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -23,15 +23,17 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -50,6 +52,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -72,6 +75,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; @@ -85,6 +89,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin vcAdmin; private final TbQueueAdmin housekeeperAdmin; private final TbQueueAdmin housekeeperReprocessingAdmin; + private final TbQueueAdmin edgeAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -96,6 +101,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbQueueTransportNotificationSettings transportNotificationSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; @@ -108,6 +114,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.jsInvokeSettings = jsInvokeSettings; this.vcSettings = vcSettings; this.consumerStatsService = consumerStatsService; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -120,6 +127,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); + this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); } @Override @@ -167,19 +175,19 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-core-notifications-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getTopic())); + requestBuilder.defaultTopic(topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { - TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + public TbQueueConsumer> createToVersionControlMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(vcSettings.getTopic())); consumerBuilder.clientId("monolith-vc-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId(topicService.buildTopicName("monolith-vc-node")); - consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(vcAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); @@ -353,8 +361,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createVersionControlMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createVersionControlMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-vc-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(vcSettings.getTopic())); @@ -408,6 +416,52 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .build(); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.buildTopicName(edgeSettings.getTopic())); + consumerBuilder.clientId("monolith-edge-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-consumer")); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(edgeAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("monolith-to-edge-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.buildTopicName(edgeSettings.getTopic())); + requestBuilder.admin(edgeAdmin); + return requestBuilder.build(); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.clientId("monolith-edge-notifications-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-notifications-consumer-" + serviceInfoProvider.getServiceId())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("monolith-to-edge-notifications-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -437,5 +491,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index f16f21ad66..b8b51706b2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -49,6 +51,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -72,6 +75,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueVersionControlSettings vcSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -84,6 +88,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin vcAdmin; private final TbQueueAdmin housekeeperAdmin; private final TbQueueAdmin housekeeperReprocessingAdmin; + private final TbQueueAdmin edgeAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -95,6 +100,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueTransportApiSettings transportApiSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { @@ -108,6 +114,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.vcSettings = vcSettings; this.consumerStatsService = consumerStatsService; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -120,6 +127,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); + this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); } @Override @@ -167,7 +175,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-to-core-notifications-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getTopic())); + requestBuilder.defaultTopic(topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -357,6 +365,51 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .build(); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.buildTopicName(edgeSettings.getTopic())); + consumerBuilder.clientId("tb-core-edge-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-consumer")); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(edgeAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-core-to-edge-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.buildTopicName(edgeSettings.getTopic())); + requestBuilder.admin(edgeAdmin); + return requestBuilder.build(); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.clientId("tb-edge-notifications-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("tb-edge-notifications-node-" + serviceInfoProvider.getServiceId())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-core-to-edge-notifications-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } @PreDestroy private void destroy() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 0b320ba617..485830caf4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -26,7 +26,9 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -47,6 +49,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -66,6 +69,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbKafkaAdmin ruleEngineAdmin; @@ -74,6 +78,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin housekeeperAdmin; + private final TbQueueAdmin edgeAdmin; private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -83,6 +88,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; @@ -92,6 +98,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.jsInvokeSettings = jsInvokeSettings; this.consumerStatsService = consumerStatsService; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -100,6 +107,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); + this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); } @Override @@ -143,8 +151,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-ota-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getOtaPackageTopic())); @@ -162,6 +170,26 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return requestBuilder.build(); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-rule-engine-to-edge-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.buildTopicName(edgeSettings.getTopic())); + requestBuilder.admin(edgeAdmin); + return requestBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-rule-engine-to-edge-notifications-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { throw new UnsupportedOperationException("Rule engine consumer should use a partitionId"); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 00f4a1ec4e..3df48e8fb8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -24,15 +24,17 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -50,6 +52,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -71,6 +74,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -78,6 +82,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public PubSubMonolithQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -88,7 +93,8 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng TbServiceInfoProvider serviceInfoProvider, TbPubSubSubscriptionSettings pubSubSubscriptionSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbQueueVersionControlSettings vcSettings) { + TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; @@ -97,6 +103,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.topicService = topicService; this.serviceInfoProvider = serviceInfoProvider; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); @@ -104,6 +111,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.transportApiAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getTransportApiSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); this.vcAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getVcSettings()); + this.edgeAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getEdgeSettings()); this.jsInvokeSettings = jsInvokeSettings; } @@ -135,9 +143,9 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbPubSubConsumerTemplate<>(vcAdmin, pubSubSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -224,7 +232,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbPubSubProducerTemplate<>(vcAdmin, pubSubSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -250,6 +258,30 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbPubSubProducerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbPubSubConsumerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -270,5 +302,8 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 091909d8f2..919d895a97 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -22,15 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -48,6 +50,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -67,12 +70,14 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueRuleEngineSettings ruleEngineSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin ruleEngineAdmin; + private final TbQueueAdmin edgeAdmin; public PubSubTbCoreQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -82,6 +87,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbQueueRuleEngineSettings ruleEngineSettings, + TbQueueEdgeSettings edgeSettings, TbPubSubSubscriptionSettings pubSubSubscriptionSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; @@ -91,12 +97,14 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; this.ruleEngineSettings = ruleEngineSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.jsExecutorAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getJsExecutorSettings()); this.transportApiAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getTransportApiSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); + this.edgeAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getEdgeSettings()); } @Override @@ -121,7 +129,8 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -194,7 +203,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { //TODO: version-control return null; } @@ -221,6 +230,30 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbPubSubConsumerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbPubSubProducerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -238,5 +271,8 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { if (ruleEngineAdmin != null) { ruleEngineAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index 3e607d0bf9..bb46610de5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -23,8 +23,10 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -46,6 +48,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -63,11 +66,13 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public PubSubTbRuleEngineQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -76,7 +81,8 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, - TbPubSubSubscriptionSettings pubSubSubscriptionSettings) { + TbPubSubSubscriptionSettings pubSubSubscriptionSettings, + TbQueueEdgeSettings edgeSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; @@ -84,11 +90,13 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); this.jsExecutorAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getJsExecutorSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); + this.edgeAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getEdgeSettings()); } @Override @@ -116,6 +124,16 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbPubSubProducerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, topicService.buildTopicName(configuration.getTopic()), diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index cf60012449..8ad400d9e3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -24,15 +24,17 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -50,6 +52,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -71,6 +74,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbRabbitMqSettings rabbitMqSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -78,6 +82,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public RabbitMqMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -87,7 +92,8 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE TbRabbitMqSettings rabbitMqSettings, TbRabbitMqQueueArguments queueArguments, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbQueueVersionControlSettings vcSettings) { + TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings) { this.topicService = topicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -97,6 +103,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.rabbitMqSettings = rabbitMqSettings; this.jsInvokeSettings = jsInvokeSettings; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); @@ -104,6 +111,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.transportApiAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getTransportApiArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); this.vcAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getVcArgs()); + this.edgeAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getEdgeArgs()); } @Override @@ -128,13 +136,14 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(vcAdmin, rabbitMqSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -221,7 +230,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbRabbitMqProducerTemplate<>(vcAdmin, rabbitMqSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -247,6 +256,30 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbRabbitMqProducerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -267,5 +300,8 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index 57440422be..16f9838384 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -22,14 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -47,6 +50,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -66,12 +70,14 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public RabbitMqTbCoreQueueFactory(TbRabbitMqSettings rabbitMqSettings, TbQueueCoreSettings coreSettings, @@ -81,6 +87,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbRabbitMqQueueArguments queueArguments) { this.rabbitMqSettings = rabbitMqSettings; this.coreSettings = coreSettings; @@ -90,12 +97,14 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); this.jsExecutorAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getJsExecutorArgs()); this.transportApiAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getTransportApiArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); + this.edgeAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getEdgeArgs()); } @Override @@ -120,7 +129,8 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -136,6 +146,30 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbRabbitMqProducerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createTransportApiRequestConsumer() { return new TbRabbitMqConsumerTemplate<>(transportApiAdmin, rabbitMqSettings, topicService.buildTopicName(transportApiSettings.getRequestsTopic()), @@ -171,7 +205,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { //TODO: version-control return null; } @@ -199,25 +233,25 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override - public TbQueueConsumer> createHousekeeperMsgConsumer() { + public TbQueueConsumer> createHousekeeperMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override - public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy @@ -237,5 +271,8 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index a0a87714ca..3dabaf100b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -26,6 +26,9 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -45,6 +48,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -62,11 +66,13 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor private final TbRabbitMqSettings rabbitMqSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public RabbitMqTbRuleEngineQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -74,6 +80,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor TbRabbitMqSettings rabbitMqSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbRabbitMqQueueArguments queueArguments) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -82,11 +89,13 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor this.rabbitMqSettings = rabbitMqSettings; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); this.jsExecutorAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getJsExecutorArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); + this.edgeAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getEdgeArgs()); } @Override @@ -114,6 +123,16 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbRabbitMqProducerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, topicService.buildTopicName(configuration.getTopic()), @@ -156,12 +175,12 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java index 9b8a3d3c49..ea922557bd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java @@ -19,9 +19,9 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; @@ -132,7 +132,7 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 2ec807498c..fa7e73b879 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -23,15 +23,17 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -49,6 +51,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -70,6 +73,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul private final TbServiceBusSettings serviceBusSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +81,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public ServiceBusMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -86,6 +91,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -96,6 +102,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul this.serviceBusSettings = serviceBusSettings; this.jsInvokeSettings = jsInvokeSettings; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); @@ -103,6 +110,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul this.transportApiAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getTransportApiConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); this.vcAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getVcConfigs()); + this.edgeAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getEdgeConfigs()); } @Override @@ -127,13 +135,14 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbServiceBusConsumerTemplate<>(vcAdmin, serviceBusSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -220,7 +229,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbServiceBusProducerTemplate<>(vcAdmin, serviceBusSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -246,6 +255,30 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbServiceBusProducerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -266,5 +299,8 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index 493e58dba7..71a7efe50b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -22,15 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -48,6 +50,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -67,12 +70,14 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public ServiceBusTbCoreQueueFactory(TbServiceBusSettings serviceBusSettings, TbQueueCoreSettings coreSettings, @@ -82,6 +87,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.serviceBusSettings = serviceBusSettings; this.coreSettings = coreSettings; @@ -91,12 +97,14 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); this.jsExecutorAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getJsExecutorConfigs()); this.transportApiAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getTransportApiConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); + this.edgeAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getEdgeConfigs()); } @Override @@ -121,7 +129,8 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -194,7 +203,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { //TODO: version-control return null; } @@ -221,6 +230,30 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbServiceBusProducerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -238,5 +271,8 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index 92ef01fa00..643a9dee3b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -26,6 +26,9 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -45,6 +48,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -62,11 +66,13 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact private final TbServiceBusSettings serviceBusSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public ServiceBusTbRuleEngineQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -74,6 +80,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -82,11 +89,13 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact this.serviceBusSettings = serviceBusSettings; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); this.jsExecutorAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getJsExecutorConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); + this.edgeAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getEdgeConfigs()); } @Override @@ -114,6 +123,16 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbServiceBusProducerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbServiceBusConsumerTemplate<>(ruleEngineAdmin, serviceBusSettings, topicService.buildTopicName(configuration.getTopic()), @@ -156,12 +175,12 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java index fd493eb2e1..7f3e246eec 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java @@ -19,9 +19,9 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; @@ -133,7 +133,7 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 20a67c2e09..cecfddf7a7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -18,6 +18,8 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -138,4 +140,12 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous TbQueueConsumer> createHousekeeperReprocessingMsgConsumer(); + TbQueueConsumer> createEdgeMsgConsumer(); + + TbQueueProducer> createEdgeMsgProducer(); + + TbQueueConsumer> createToEdgeNotificationsMsgConsumer(); + + TbQueueProducer> createEdgeNotificationsMsgProducer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 3fcb49f47c..cf94bf68d0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -19,6 +19,8 @@ import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -39,6 +41,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCore; private TbQueueProducer> toRuleEngineNotifications; private TbQueueProducer> toTbCoreNotifications; + private TbQueueProducer> toEdge; + private TbQueueProducer> toEdgeNotifications; private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; private TbQueueProducer> toHousekeeper; @@ -57,6 +61,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toVersionControl = tbQueueProvider.createVersionControlMsgProducer(); this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); + this.toEdge = tbQueueProvider.createEdgeMsgProducer(); + this.toEdgeNotifications = tbQueueProvider.createEdgeNotificationsMsgProducer(); } @Override @@ -99,4 +105,15 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toHousekeeper; } + + @Override + public TbQueueProducer> getTbEdgeMsgProducer() { + return toEdge; + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + return toEdgeNotifications; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index a752301cd8..a0c43a9e4b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -17,6 +17,8 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -73,7 +75,7 @@ public interface TbQueueProducerProvider { */ TbQueueProducer> getTbUsageStatsMsgProducer(); - /** + /** * Used to push messages to other instances of TB Core Service * * @return @@ -82,4 +84,8 @@ public interface TbQueueProducerProvider { TbQueueProducer> getHousekeeperMsgProducer(); + TbQueueProducer> getTbEdgeMsgProducer(); + + TbQueueProducer> getTbEdgeNotificationsMsgProducer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index 908369ea15..8797064204 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -18,14 +18,16 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -41,6 +43,8 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCoreNotifications; private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; + private TbQueueProducer> toEdge; + private TbQueueProducer> toEdgeNotifications; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -55,6 +59,8 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); + this.toEdge = tbQueueProvider.createEdgeMsgProducer(); + this.toEdgeNotifications = tbQueueProvider.createEdgeNotificationsMsgProducer(); } @Override @@ -82,13 +88,23 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { return toTbCoreNotifications; } + @Override + public TbQueueProducer> getTbEdgeMsgProducer() { + return toEdge; + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + return toEdgeNotifications; + } + @Override public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } @Override - public TbQueueProducer> getTbVersionControlMsgProducer() { + public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Rule Engine!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index e49074f92d..dda3f33ed4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -19,6 +19,9 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -67,14 +70,18 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory * * @return */ - TbQueueProducer> createTbCoreNotificationsMsgProducer(); + TbQueueProducer> createTbCoreNotificationsMsgProducer(); + + TbQueueProducer> createEdgeMsgProducer(); + + TbQueueProducer> createEdgeNotificationsMsgProducer(); /** * Used to consume messages about firmware update notifications to TB Core Service * * @return */ - TbQueueProducer> createToOtaPackageStateServiceMsgProducer(); + TbQueueProducer> createToOtaPackageStateServiceMsgProducer(); /** * Used to consume messages by TB Rule Engine Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index df394cd997..b2c7856b20 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -18,14 +18,16 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -79,7 +81,17 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbVersionControlMsgProducer() { + public TbQueueProducer> getTbEdgeMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used Transport!"); + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Transport!"); + } + + @Override + public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 7547c73f71..a8f3b124e2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -18,14 +18,16 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -75,7 +77,17 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbVersionControlMsgProducer() { + public TbQueueProducer> getTbEdgeMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } + + @Override + public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java index 2faf4b1985..14aa67a4f0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java @@ -27,6 +27,7 @@ import java.util.Map; @Component @ConditionalOnExpression("'${queue.type:null}'=='pubsub'") public class TbPubSubSubscriptionSettings { + @Value("${queue.pubsub.queue-properties.core:}") private String coreProperties; @Value("${queue.pubsub.queue-properties.rule-engine:}") @@ -39,6 +40,8 @@ public class TbPubSubSubscriptionSettings { private String jsExecutorProperties; @Value("${queue.pubsub.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.pubsub.queue-properties.edge:}") + private String edgeProperties; @Getter private Map coreSettings; @@ -52,6 +55,8 @@ public class TbPubSubSubscriptionSettings { private Map jsExecutorSettings; @Getter private Map vcSettings; + @Getter + private Map edgeSettings; @PostConstruct private void init() { @@ -61,6 +66,7 @@ public class TbPubSubSubscriptionSettings { notificationsSettings = PropertyUtils.getProps(notificationsProperties); jsExecutorSettings = PropertyUtils.getProps(jsExecutorProperties); vcSettings = PropertyUtils.getProps(vcProperties); + edgeSettings = PropertyUtils.getProps(edgeProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java index acd008fb1d..06613d5617 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java @@ -41,6 +41,8 @@ public class TbRabbitMqQueueArguments { private String jsExecutorProperties; @Value("${queue.rabbitmq.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.rabbitmq.queue-properties.edge:}") + private String edgeProperties; @Getter private Map coreArgs; @@ -54,6 +56,8 @@ public class TbRabbitMqQueueArguments { private Map jsExecutorArgs; @Getter private Map vcArgs; + @Getter + private Map edgeArgs; @PostConstruct private void init() { @@ -63,6 +67,7 @@ public class TbRabbitMqQueueArguments { notificationsArgs = getArgs(notificationsProperties); jsExecutorArgs = getArgs(jsExecutorProperties); vcArgs = getArgs(vcProperties); + edgeArgs = getArgs(edgeProperties); } public static Map getArgs(String properties) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java new file mode 100644 index 0000000000..87fefc8113 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java @@ -0,0 +1,31 @@ +/** + * Copyright ยฉ 2016-2024 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.queue.settings; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Lazy +@Data +@Component +public class TbQueueEdgeSettings { + + @Value("${queue.edge.topic}") + private String topic; + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java index 0c61c2d2eb..2c1b8793eb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java @@ -43,6 +43,8 @@ public class TbAwsSqsQueueAttributes { private String otaProperties; @Value("${queue.aws-sqs.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.aws-sqs.queue-properties.edge:}") + private String edgeProperties; @Getter private Map coreAttributes; @@ -58,6 +60,8 @@ public class TbAwsSqsQueueAttributes { private Map otaAttributes; @Getter private Map vcAttributes; + @Getter + private Map edgeAttributes; private final Map defaultAttributes = new HashMap<>(); @@ -72,6 +76,7 @@ public class TbAwsSqsQueueAttributes { jsExecutorAttributes = getConfigs(jsExecutorProperties); otaAttributes = getConfigs(otaProperties); vcAttributes = getConfigs(vcProperties); + edgeAttributes = getConfigs(edgeProperties); } private Map getConfigs(String properties) { diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java index 6c854ac8d6..3833155c05 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java @@ -21,9 +21,10 @@ public enum StatsType { TRANSPORT("transport"), JS_INVOKE("jsInvoke"), RATE_EXECUTOR("rateExecutor"), - HOUSEKEEPER("housekeeper"); + HOUSEKEEPER("housekeeper"), + EDGE("edge"); - private String name; + private final String name; StatsType(String name) { this.name = name; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index 99401bbdf0..138505bd8d 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -79,6 +79,7 @@ import org.thingsboard.server.queue.util.TbVersionControlComponent; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -174,7 +175,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe } } } - consumer.subscribe(event.getPartitions()); + consumer.subscribe(event.getPartitionsMap().values().stream().findAny().orElse(Collections.emptySet())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 6a9536d8ac..1e0202bf79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -17,11 +17,9 @@ package org.thingsboard.server.dao.edge; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import jakarta.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -54,6 +52,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -97,8 +96,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService() { - @Nullable - @Override - public List apply(@Nullable List edgeList) { - return edgeList == null ? - Collections.emptyList() : - edgeList.stream().filter(edge -> query.getEdgeTypes().contains(edge.getType())).collect(Collectors.toList()); - } - }, MoreExecutors.directExecutor()); - + edges = Futures.transform(edges, edgeList -> edgeList == null ? + Collections.emptyList() : + edgeList.stream().filter(edge -> query.getEdgeTypes().contains(edge.getType())).collect(Collectors.toList()), MoreExecutors.directExecutor()); return edges; } @@ -417,19 +408,11 @@ public class EdgeServiceImpl extends AbstractCachedEntityService pageData; - do { - pageData = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId, pageLink); - if (pageData.getData().size() > 0) { - for (RuleChain ruleChain : pageData.getData()) { - ruleChainService.assignRuleChainToEdge(tenantId, ruleChain.getId(), edgeId); - } - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + PageDataIterable ruleChains = new PageDataIterable<>( + link -> ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId, link), 1024); + for (RuleChain ruleChain : ruleChains) { + ruleChainService.assignRuleChainToEdge(tenantId, ruleChain.getId(), edgeId); + } } @Override @@ -580,6 +563,17 @@ public class EdgeServiceImpl extends AbstractCachedEntityService isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String key) { ListenableFuture> futureKvEntry; @@ -594,17 +588,11 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) { List result = new ArrayList<>(); - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - do { - pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - result.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); + PageDataIterable ruleChains = new PageDataIterable<>( + link -> ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, link), 1024); + for (RuleChain ruleChain : ruleChains) { + result.add(ruleChain); + } return result; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java new file mode 100644 index 0000000000..948b860649 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright ยฉ 2016-2024 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.dao.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.EdgeId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("EdgeSessionCache") +public class EdgeSessionCaffeineCache extends CaffeineTbTransactionalCache { + + public EdgeSessionCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.EDGE_SESSIONS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java new file mode 100644 index 0000000000..518c0c0b32 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright ยฉ 2016-2024 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.dao.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.EdgeId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("EdgeSessionCache") +public class EdgeSessionRedisCache extends RedisTbTransactionalCache { + + public EdgeSessionRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.EDGE_SESSIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(String.class)); + } + +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 48f7c92b1d..e71c6b1294 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -75,6 +75,9 @@ cache.specs.otaPackagesData.maxSize=100000 cache.specs.edges.timeToLiveInMinutes=1440 cache.specs.edges.maxSize=100000 +cache.specs.edgeSessions.timeToLiveInMinutes=1440 +cache.specs.edgeSessions.maxSize=100000 + cache.specs.relatedEdges.timeToLiveInMinutes=1440 cache.specs.relatedEdges.maxSize=100000 From 4831a86295cde415b2ce2512a20f17b1d78fac3d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 29 Aug 2024 13:16:56 +0300 Subject: [PATCH 310/331] Added UI part of RuleChain Editor Version Conflict --- .../rulechain/rulechain-page.component.ts | 50 ++++++++++++------- .../entity-conflict-dialog.component.html | 2 +- .../entity-conflict-dialog.component.ts | 12 +++-- .../import-export/import-export.service.ts | 30 +++++++---- ui-ngx/src/app/shared/models/entity.models.ts | 5 ++ .../app/shared/models/rule-chain.models.ts | 2 +- 6 files changed, 69 insertions(+), 32 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 46c84087ee..eb640a3af8 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -77,8 +77,8 @@ import { } from '@shared/models/rule-node.models'; import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; import { RuleChainService } from '@core/http/rule-chain.service'; -import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject, throwError } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { ISearchableComponent } from '../../models/searchable-component.models'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; @@ -93,6 +93,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { MatDrawer } from '@angular/material/sidenav'; +import { HttpStatusCode } from '@angular/common/http'; import Timeout = NodeJS.Timeout; @Component({ @@ -1282,7 +1283,9 @@ export class RuleChainPageComponent extends PageComponent onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) { const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent; const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent; - if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) { + if (ruleNodeConfigComponent.useDefinedDirective() + && ruleNodeConfigDefinedComponent.hasScript + && ruleNodeConfigDefinedComponent.testScript) { ruleNodeConfigDefinedComponent.testScript(debugEventBody); } } @@ -1456,7 +1459,8 @@ export class RuleChainPageComponent extends PageComponent const ruleChainMetaData: RuleChainMetaData = { ruleChainId: this.ruleChain.id, nodes: [], - connections: [] + connections: [], + version: ruleChain.version }; const nodes: FcRuleNode[] = []; this.ruleChainModel.nodes.forEach((node) => { @@ -1465,7 +1469,9 @@ export class RuleChainPageComponent extends PageComponent id: node.ruleNodeId, type: node.component.clazz, name: node.name, - configurationVersion: isDefinedAndNotNull(node.configurationVersion) ? node.configurationVersion : node.component.configurationVersion, + configurationVersion: isDefinedAndNotNull(node.configurationVersion) + ? node.configurationVersion + : node.component.configurationVersion, configuration: node.configuration, additionalInfo: node.additionalInfo ? node.additionalInfo : {}, debugMode: node.debugMode, @@ -1500,20 +1506,30 @@ export class RuleChainPageComponent extends PageComponent }); } }); - this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { - this.ruleChainMetaData = savedRuleChainMetaData; - if (this.isImport) { - this.isDirtyValue = false; - this.isImport = false; - if (this.ruleChainType !== RuleChainType.EDGE) { - this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData) + .pipe( + catchError(err => { + if (err.status === HttpStatusCode.Conflict) { + return this.ruleChainService.getRuleChainMetadata(ruleChainMetaData.ruleChainId.id); + } + return throwError(() => err); + }) + ) + .subscribe((savedRuleChainMetaData) => { + this.ruleChain.version = savedRuleChainMetaData.version; + this.ruleChainMetaData = savedRuleChainMetaData; + if (this.isImport) { + this.isDirtyValue = false; + this.isImport = false; + if (this.ruleChainType !== RuleChainType.EDGE) { + this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + } else { + this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); + } } else { - this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); + this.createRuleChainModel(); } - } else { - this.createRuleChainModel(); - } - saveResult.next(); + saveResult.next(); }); }); return saveResult; diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html index ba09311895..2e16f04498 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html @@ -30,7 +30,7 @@
{{ 'entity.version-conflict.link' | translate: - { entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) } + { entityType: (entityTypeTranslations.get(entityId.entityType).type | translate) } }} {{ 'entity.link' | translate }}. diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index 37275bd79c..c0879d8ac2 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -20,11 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; -import { EntityInfoData } from '@shared/models/entity.models'; +import { EntityDTOInfoData, EntityInfoData } from '@shared/models/entity.models'; +import { EntityId } from '@shared/models/id/entity-id'; interface EntityConflictDialogData { message: string; - entity: EntityInfoData; + entity: EntityInfoData | EntityDTOInfoData; } @Component({ @@ -38,13 +39,18 @@ interface EntityConflictDialogData { ], }) export class EntityConflictDialogComponent { + + entityId: EntityId; + readonly entityTypeTranslations = entityTypeTranslations; constructor( @Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData, private dialogRef: MatDialogRef, private importExportService: ImportExportService, - ) {} + ) { + this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as EntityDTOInfoData).ruleChainId; + } onCancel(): void { this.dialogRef.close(); diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 4746365c50..be25786d5d 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -55,7 +55,12 @@ import { EntityType } from '@shared/models/entity-type.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; -import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; +import { + EntityDTOInfoData, + EntityInfoData, + ImportEntitiesResultInfo, + ImportEntityData +} from '@shared/models/entity.models'; import { RequestConfig } from '@core/http/http-utils'; import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; @@ -360,25 +365,26 @@ export class ImportExportService { }); } - public exportEntity(entityData: EntityInfoData): void { + public exportEntity(entityData: EntityInfoData | EntityDTOInfoData): void { + const id = (entityData as EntityInfoData).id ?? (entityData as EntityDTOInfoData).ruleChainId; let preparedData; - switch (entityData.id.entityType) { + switch (id.entityType) { case EntityType.DEVICE_PROFILE: case EntityType.ASSET_PROFILE: preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile); break; case EntityType.RULE_CHAIN: - this.ruleChainService.getRuleChainMetadata(entityData.id.id) + forkJoin([this.ruleChainService.getRuleChainMetadata(id.id), this.ruleChainService.getRuleChain(id.id)]) .pipe( take(1), - map((ruleChainMetaData) => { + map(([ruleChainMetaData, ruleChain]) => { const ruleChainExport: RuleChainImport = { - ruleChain: this.prepareRuleChain(entityData as RuleChain), + ruleChain: this.prepareRuleChain(ruleChain), metadata: this.prepareRuleChainMetaData(ruleChainMetaData) }; return ruleChainExport; })) - .subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name)); + .subscribe(this.onRuleChainExported()); return; case EntityType.WIDGETS_BUNDLE: this.exportSelectedWidgetsBundle(entityData as WidgetsBundle); @@ -584,8 +590,12 @@ export class ImportExportService { return ruleChainExport; }) )) - ).subscribe({ - next: (ruleChainExport) => { + ).subscribe(this.onRuleChainExported()); + } + + private onRuleChainExported() { + return { + next: (ruleChainExport: RuleChainImport) => { let name = ruleChainExport.ruleChain.name; name = name.toLowerCase().replace(/\W/g, '_'); this.exportToPc(ruleChainExport, name); @@ -593,7 +603,7 @@ export class ImportExportService { error: (e) => { this.handleExportError(e, 'rulechain.export-failed-error'); } - }); + }; } public importRuleChain(expectedRuleChainType: RuleChainType): Observable { diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 40ec570b13..5d9a3f6f04 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -34,6 +34,11 @@ export interface EntityInfoData { name: string; } +export interface EntityDTOInfoData { + ruleChainId: EntityId; + name: string; +} + export interface ImportEntityData { lineNumber: number; name: string; diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 0bfdfbc459..bca4ae399e 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -34,7 +34,7 @@ export interface RuleChain extends BaseData, HasTenantId, HasVersio isDefault?: boolean; } -export interface RuleChainMetaData { +export interface RuleChainMetaData extends HasVersion { ruleChainId: RuleChainId; firstNodeIndex?: number; nodes: Array; From 61e7ba46fcbcdc288343c1193a0bffc90ebdfcda Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 29 Aug 2024 15:25:20 +0300 Subject: [PATCH 311/331] Adjusted typing --- .../entity-conflict-dialog.component.ts | 7 ++++--- .../src/app/shared/import-export/import-export.service.ts | 7 +++---- ui-ngx/src/app/shared/models/entity.models.ts | 5 ----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index c0879d8ac2..b4f34ed232 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -20,12 +20,13 @@ import { SharedModule } from '@shared/shared.module'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; -import { EntityDTOInfoData, EntityInfoData } from '@shared/models/entity.models'; +import { EntityInfoData } from '@shared/models/entity.models'; import { EntityId } from '@shared/models/id/entity-id'; +import { RuleChainMetaData } from '@shared/models/rule-chain.models'; interface EntityConflictDialogData { message: string; - entity: EntityInfoData | EntityDTOInfoData; + entity: EntityInfoData | RuleChainMetaData; } @Component({ @@ -49,7 +50,7 @@ export class EntityConflictDialogComponent { private dialogRef: MatDialogRef, private importExportService: ImportExportService, ) { - this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as EntityDTOInfoData).ruleChainId; + this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as RuleChainMetaData).ruleChainId; } onCancel(): void { diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index be25786d5d..5f246873d7 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -56,7 +56,6 @@ import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { - EntityDTOInfoData, EntityInfoData, ImportEntitiesResultInfo, ImportEntityData @@ -365,8 +364,8 @@ export class ImportExportService { }); } - public exportEntity(entityData: EntityInfoData | EntityDTOInfoData): void { - const id = (entityData as EntityInfoData).id ?? (entityData as EntityDTOInfoData).ruleChainId; + public exportEntity(entityData: EntityInfoData | RuleChainMetaData): void { + const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; let preparedData; switch (id.entityType) { case EntityType.DEVICE_PROFILE: @@ -395,7 +394,7 @@ export class ImportExportService { default: preparedData = this.prepareExport(entityData); } - this.exportToPc(preparedData, entityData.name); + this.exportToPc(preparedData, (entityData as EntityInfoData).name); } private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void { diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 5d9a3f6f04..40ec570b13 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -34,11 +34,6 @@ export interface EntityInfoData { name: string; } -export interface EntityDTOInfoData { - ruleChainId: EntityId; - name: string; -} - export interface ImportEntityData { lineNumber: number; name: string; From d6c40c1bb20ce4b44e9575fdb4676bbe5060645d Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Thu, 29 Aug 2024 18:28:36 +0300 Subject: [PATCH 312/331] UI: Recent dashboards widget - apply custom translation for dashboard titles --- .../lib/home-page/recent-dashboards-widget.component.html | 4 ++-- .../shared/components/dashboard-autocomplete.component.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html index 72b5d8a7f9..6c73e47574 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html @@ -48,7 +48,7 @@ {{ 'widgets.recent-dashboards.name' | translate }} - {{ lastVisitedDashboard.title }} + {{ lastVisitedDashboard.title | customTranslate }} @@ -78,7 +78,7 @@ class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}
diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html index 6512aa4e73..13cf1bd664 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html @@ -32,7 +32,7 @@ [matAutocomplete]="dashboardAutocomplete" [fxShow]="!useDashboardLink || !disabled || !selectDashboardFormGroup.get('dashboard').value"> - {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) }} + {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }} + + + {{country.flag}} + + + +
+
+ contact.no-country-found +
+ + + {{ 'contact.no-country-matching' | translate: + {country: (searchText | truncate: true: 6: '...')} }} + + +
+
+
+ {{ autocompleteHint }} + + {{ requiredText }} + + diff --git a/ui-ngx/src/app/shared/components/country-autocomplete.component.ts b/ui-ngx/src/app/shared/components/country-autocomplete.component.ts new file mode 100644 index 0000000000..7fc3a10bca --- /dev/null +++ b/ui-ngx/src/app/shared/components/country-autocomplete.component.ts @@ -0,0 +1,209 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { Country, CountryData } from '@shared/models/country.models'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { isNotEmptyStr } from '@core/utils'; +import { Observable, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { SubscriptSizing } from '@angular/material/form-field'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TranslateService } from '@ngx-translate/core'; + +interface CountrySearchData extends Country { + searchText?: string; +} + +@Component({ + selector: 'tb-country-autocomplete', + templateUrl: 'country-autocomplete.component.html', + providers: [ + CountryData, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CountryAutocompleteComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CountryAutocompleteComponent), + multi: true + } + ] +}) +export class CountryAutocompleteComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + labelText = this.translate.instant('contact.country'); + + @Input() + requiredText = this.translate.instant('contact.country-required'); + + @Input() + autocompleteHint: string; + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + required = false; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @ViewChild('countryInput', {static: true}) countryInput: ElementRef; + + @Output() + selectCountryCode = new EventEmitter(); + + countryFormGroup: FormGroup; + + searchText = ''; + + filteredCountries: Observable>; + + onTouched = () => { + }; + private propagateChange: (value: any) => void = () => { + }; + + private modelValue: Country; + + private allCountries: CountrySearchData[] = this.countryData.allCountries; + private initSearchData = false; + private dirty = false; + + constructor(private fb: FormBuilder, + private countryData: CountryData, + private translate: TranslateService) { + this.countryFormGroup = this.fb.group({ + country: [''] + }); + } + + ngOnInit(): void { + this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe( + debounceTime(150), + tap(value => { + let modelValue: Country; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + distinctUntilChanged(), + switchMap(name => of(this.fetchCountries(name))), + share() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.countryFormGroup.disable({emitEvent: false}); + } else { + this.countryFormGroup.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + return this.countryFormGroup.valid ? null : { + countryFormGroup: false + }; + } + + writeValue(country: string) { + this.dirty = true; + + const findCountry = isNotEmptyStr(country) ? this.allCountries.find(item => item.name === country) : null; + + this.modelValue = findCountry || null; + this.countryFormGroup.get('country').patchValue(this.modelValue || '', { emitEvent: false }); + this.selectCountryCode.emit(this.modelValue ? this.modelValue.iso2 : null); + } + + displayCountryFn(country?: Country): string | undefined { + return country ? `${country.flag} ${country.name}` : undefined; + } + + onFocus() { + if (this.dirty) { + this.countryFormGroup.get('country').updateValueAndValidity({onlySelf: true}); + this.dirty = false; + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + clear() { + this.countryFormGroup.get('country').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.countryInput.nativeElement.blur(); + this.countryInput.nativeElement.focus(); + }, 0); + } + + private fetchCountries(searchText: string): Country[] { + this.searchText = searchText; + if (!this.initSearchData) { + this.allCountries.forEach(country => { + country.searchText = `${country.name} ${country.iso2}`.toLowerCase(); + }); + this.initSearchData = true; + } + if (isNotEmptyStr(searchText)) { + const filterValue = searchText.toLowerCase(); + return this.allCountries.filter(country => country.searchText.includes(filterValue)); + } + return this.allCountries; + } + + private updateView(value: Country | null) { + if (this.modelValue?.name !== value?.name) { + this.modelValue = value; + this.propagateChange(this.modelValue); + if (value) { + this.selectCountryCode.emit(value.iso2); + } + } + } +} diff --git a/ui-ngx/src/app/shared/models/country.models.ts b/ui-ngx/src/app/shared/models/country.models.ts index 919d362067..d8563e2449 100644 --- a/ui-ngx/src/app/shared/models/country.models.ts +++ b/ui-ngx/src/app/shared/models/country.models.ts @@ -22,6 +22,7 @@ export interface Country { dialCode: string; areaCodes?: string[]; flag: string; + postCodePattern?: RegExp; } export enum CountryISO { @@ -80,10 +81,10 @@ export enum CountryISO { CongoRepublicCongoBrazzaville = 'CG', CookIslands = 'CK', CostaRica = 'CR', - CรดteDIvoire = 'CI', + CoteDivoire = 'CI', Croatia = 'HR', Cuba = 'CU', - Curaรงao = 'CW', + Curacao = 'CW', Cyprus = 'CY', CzechRepublic = 'CZ', Denmark = 'DK', @@ -155,7 +156,6 @@ export enum CountryISO { Lithuania = 'LT', Luxembourg = 'LU', Macau = 'MO', - Macedonia = 'MK', Madagascar = 'MG', Malawi = 'MW', Malaysia = 'MY', @@ -189,6 +189,7 @@ export enum CountryISO { Niue = 'NU', NorfolkIsland = 'NF', NorthKorea = 'KP', + NorthMacedonia = 'MK', NorthernMarianaIslands = 'MP', Norway = 'NO', Oman = 'OM', @@ -205,11 +206,11 @@ export enum CountryISO { Portugal = 'PT', PuertoRico = 'PR', Qatar = 'QA', - Rรฉunion = 'RE', + Reunion = 'RE', Romania = 'RO', Russia = 'RU', Rwanda = 'RW', - SaintBarthรฉlemy = 'BL', + SaintBarthelemy = 'BL', SaintHelena = 'SH', SaintKittsAndNevis = 'KN', SaintLucia = 'LC', @@ -218,7 +219,7 @@ export enum CountryISO { SaintVincentAndTheGrenadines = 'VC', Samoa = 'WS', SanMarino = 'SM', - SรฃoTomรฉAndPrรญncipe = 'ST', + SaoTomeAndPrincipe = 'ST', SaudiArabia = 'SA', Senegal = 'SN', Serbia = 'RS', @@ -274,9 +275,10 @@ export enum CountryISO { Yemen = 'YE', Zambia = 'ZM', Zimbabwe = 'ZW', - ร…landIslands = 'AX', + AlandIslands = 'AX', } +/* eslint-disable max-len */ @Injectable() export class CountryData { public allCountries: Array = [ @@ -292,15 +294,15 @@ export class CountryData { {name: 'Argentina', iso2: CountryISO.Argentina, dialCode: '54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท'}, {name: 'Armenia', iso2: CountryISO.Armenia, dialCode: '374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ'}, {name: 'Aruba', iso2: CountryISO.Aruba, dialCode: '297', flag: '๐Ÿ‡ฆ๐Ÿ‡ผ'}, - {name: 'Australia', iso2: CountryISO.Australia, dialCode: '61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ'}, - {name: 'Austria', iso2: CountryISO.Austria, dialCode: '43', flag: '๐Ÿ‡ฆ๐Ÿ‡น'}, + {name: 'Australia', iso2: CountryISO.Australia, dialCode: '61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ', postCodePattern: /[0-9]{4}/}, + {name: 'Austria', iso2: CountryISO.Austria, dialCode: '43', flag: '๐Ÿ‡ฆ๐Ÿ‡น', postCodePattern: /[0-9]{4}/}, {name: 'Azerbaijan', iso2: CountryISO.Azerbaijan, dialCode: '994', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ'}, {name: 'Bahamas', iso2: CountryISO.Bahamas, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ธ'}, {name: 'Bahrain', iso2: CountryISO.Bahrain, dialCode: '973', flag: '๐Ÿ‡ง๐Ÿ‡ญ'}, {name: 'Bangladesh', iso2: CountryISO.Bangladesh, dialCode: '880', flag: '๐Ÿ‡ง๐Ÿ‡ฉ'}, {name: 'Barbados', iso2: CountryISO.Barbados, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ง'}, {name: 'Belarus', iso2: CountryISO.Belarus, dialCode: '375', flag: '๐Ÿ‡ง๐Ÿ‡พ'}, - {name: 'Belgium', iso2: CountryISO.Belgium, dialCode: '32', flag: '๐Ÿ‡ง๐Ÿ‡ช'}, + {name: 'Belgium', iso2: CountryISO.Belgium, dialCode: '32', flag: '๐Ÿ‡ง๐Ÿ‡ช', postCodePattern: /[0-9]{4}/}, {name: 'Belize', iso2: CountryISO.Belize, dialCode: '501', flag: '๐Ÿ‡ง๐Ÿ‡ฟ'}, {name: 'Benin', iso2: CountryISO.Benin, dialCode: '229', flag: '๐Ÿ‡ง๐Ÿ‡ฏ'}, {name: 'Bermuda', iso2: CountryISO.Bermuda, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ฒ'}, @@ -310,16 +312,16 @@ export class CountryData { {name: 'Bouvet Island', iso2: CountryISO.BouvetIsland, dialCode: '47', flag: '๐Ÿ‡ง๐Ÿ‡ป'}, {name: 'Bosnia and Herzegovina', iso2: CountryISO.BosniaAndHerzegovina, dialCode: '387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ'}, {name: 'Botswana', iso2: CountryISO.Botswana, dialCode: '267', flag: '๐Ÿ‡ง๐Ÿ‡ผ'}, - {name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '๐Ÿ‡ง๐Ÿ‡ท'}, + {name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '๐Ÿ‡ง๐Ÿ‡ท', postCodePattern: /[0-9]{5}-?[0-9]{3}/}, {name: 'British Indian Ocean Territory', iso2: CountryISO.BritishIndianOceanTerritory, dialCode: '246', flag: '๐Ÿ‡ฎ๐Ÿ‡ด'}, {name: 'British Virgin Islands', iso2: CountryISO.BritishVirginIslands, dialCode: '1', flag: '๐Ÿ‡ป๐Ÿ‡ฌ'}, - {name: 'Brunei', iso2: CountryISO.Brunei, dialCode: '673', flag: '๐Ÿ‡ง๐Ÿ‡ณ'}, + {name: 'Brunei Darussalam', iso2: CountryISO.Brunei, dialCode: '673', flag: '๐Ÿ‡ง๐Ÿ‡ณ'}, {name: 'Bulgaria', iso2: CountryISO.Bulgaria, dialCode: '359', flag: '๐Ÿ‡ง๐Ÿ‡ฌ'}, {name: 'Burkina Faso', iso2: CountryISO.BurkinaFaso, dialCode: '226', flag: '๐Ÿ‡ง๐Ÿ‡ซ'}, {name: 'Burundi', iso2: CountryISO.Burundi, dialCode: '257', flag: '๐Ÿ‡ง๐Ÿ‡ฎ'}, {name: 'Cambodia', iso2: CountryISO.Cambodia, dialCode: '855', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ'}, {name: 'Cameroon', iso2: CountryISO.Cameroon, dialCode: '237', flag: '๐Ÿ‡จ๐Ÿ‡ฒ'}, - {name: 'Canada', iso2: CountryISO.Canada, dialCode: '1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ'}, + {name: 'Canada', iso2: CountryISO.Canada, dialCode: '1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ', postCodePattern: /^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$/}, {name: 'Cape Verde', iso2: CountryISO.CapeVerde, dialCode: '238', flag: '๐Ÿ‡จ๐Ÿ‡ป'}, {name: 'Caribbean Netherlands', iso2: CountryISO.CaribbeanNetherlands, dialCode: '599', flag: '๐Ÿ‡ง๐Ÿ‡ถ'}, {name: 'Cayman Islands', iso2: CountryISO.CaymanIslands, dialCode: '1', flag: '๐Ÿ‡ฐ๐Ÿ‡พ'}, @@ -328,20 +330,20 @@ export class CountryData { {name: 'Chile', iso2: CountryISO.Chile, dialCode: '56', flag: '๐Ÿ‡จ๐Ÿ‡ฑ'}, {name: 'China', iso2: CountryISO.China, dialCode: '86', flag: '๐Ÿ‡จ๐Ÿ‡ณ'}, {name: 'Christmas Island', iso2: CountryISO.ChristmasIsland, dialCode: '61', flag: '๐Ÿ‡จ๐Ÿ‡ฝ'}, - {name: 'Cocos Islands', iso2: CountryISO.Cocos, dialCode: '61', flag: '๐Ÿ‡จ๐Ÿ‡จ'}, + {name: 'Cocos (Keeling) Islands', iso2: CountryISO.Cocos, dialCode: '61', flag: '๐Ÿ‡จ๐Ÿ‡จ'}, {name: 'Colombia', iso2: CountryISO.Colombia, dialCode: '57', flag: '๐Ÿ‡จ๐Ÿ‡จ'}, {name: 'Comoros', iso2: CountryISO.Comoros, dialCode: '269', flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ'}, - {name: 'Congo-Kinshasa', iso2: CountryISO.CongoDRCJamhuriYaKidemokrasiaYaKongo, dialCode: '243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ'}, - {name: 'Congo-Brazzaville', iso2: CountryISO.CongoRepublicCongoBrazzaville, dialCode: '242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ'}, + {name: 'Congo', iso2: CountryISO.CongoDRCJamhuriYaKidemokrasiaYaKongo, dialCode: '243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ'}, + {name: 'Congo, Democratic Republic of the', iso2: CountryISO.CongoRepublicCongoBrazzaville, dialCode: '242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ'}, {name: 'Cook Islands', iso2: CountryISO.CookIslands, dialCode: '682', flag: '๐Ÿ‡จ๐Ÿ‡ฐ'}, {name: 'Costa Rica', iso2: CountryISO.CostaRica, dialCode: '506', flag: '๐Ÿ‡จ๐Ÿ‡ท'}, - {name: 'Cรดte dโ€™Ivoire', iso2: CountryISO.CรดteDIvoire, dialCode: '225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ'}, + {name: 'Cรดte d\'Ivoire', iso2: CountryISO.CoteDivoire, dialCode: '225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ'}, {name: 'Croatia', iso2: CountryISO.Croatia, dialCode: '385', flag: '๐Ÿ‡ญ๐Ÿ‡ท'}, {name: 'Cuba', iso2: CountryISO.Cuba, dialCode: '53', flag: '๐Ÿ‡จ๐Ÿ‡บ'}, - {name: 'Curaรงao', iso2: CountryISO.Curaรงao, dialCode: '599', flag: '๐Ÿ‡จ๐Ÿ‡ผ'}, + {name: 'Curaรงao', iso2: CountryISO.Curacao, dialCode: '599', flag: '๐Ÿ‡จ๐Ÿ‡ผ'}, {name: 'Cyprus', iso2: CountryISO.Cyprus, dialCode: '357', flag: '๐Ÿ‡จ๐Ÿ‡พ'}, {name: 'Czech Republic', iso2: CountryISO.CzechRepublic, dialCode: '420', flag: '๐Ÿ‡จ๐Ÿ‡ฟ'}, - {name: 'Denmark', iso2: CountryISO.Denmark, dialCode: '45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ'}, + {name: 'Denmark', iso2: CountryISO.Denmark, dialCode: '45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ', postCodePattern: /[0-9]{3,4}/}, {name: 'Djibouti', iso2: CountryISO.Djibouti, dialCode: '253', flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ'}, {name: 'Dominica', iso2: CountryISO.Dominica, dialCode: '1767', flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ'}, {name: 'Dominican Republic', iso2: CountryISO.DominicanRepublic, dialCode: '1', flag: '๐Ÿ‡ฉ๐Ÿ‡ด'}, @@ -352,8 +354,8 @@ export class CountryData { {name: 'Eritrea', iso2: CountryISO.Eritrea, dialCode: '291', flag: '๐Ÿ‡ช๐Ÿ‡ท'}, {name: 'Estonia', iso2: CountryISO.Estonia, dialCode: '372', flag: '๐Ÿ‡ช๐Ÿ‡ช'}, {name: 'Ethiopia', iso2: CountryISO.Ethiopia, dialCode: '251', flag: '๐Ÿ‡ช๐Ÿ‡น'}, - {name: 'Falkland Islands', iso2: CountryISO.FalklandIslands, dialCode: '500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ'}, - {name: 'Faroe Islands', iso2: CountryISO.FaroeIslands, dialCode: '298', flag: '๐Ÿ‡ซ๐Ÿ‡ด'}, + {name: 'Falkland Islands (Malvinas)', iso2: CountryISO.FalklandIslands, dialCode: '500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ'}, + {name: 'Faroe Islands', iso2: CountryISO.FaroeIslands, dialCode: '298', flag: '๐Ÿ‡ซ๐Ÿ‡ด', postCodePattern: /[0-9]{3,4}/}, {name: 'Fiji', iso2: CountryISO.Fiji, dialCode: '679', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ'}, {name: 'Finland', iso2: CountryISO.Finland, dialCode: '358', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ'}, {name: 'France', iso2: CountryISO.France, dialCode: '33', flag: '๐Ÿ‡ซ๐Ÿ‡ท'}, @@ -363,7 +365,7 @@ export class CountryData { {name: 'Gabon', iso2: CountryISO.Gabon, dialCode: '241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ'}, {name: 'Gambia', iso2: CountryISO.Gambia, dialCode: '220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ'}, {name: 'Georgia', iso2: CountryISO.Georgia, dialCode: '995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช'}, - {name: 'Germany', iso2: CountryISO.Germany, dialCode: '49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช'}, + {name: 'Germany', iso2: CountryISO.Germany, dialCode: '49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช', postCodePattern: /[0-9]{5}/}, {name: 'Ghana', iso2: CountryISO.Ghana, dialCode: '233', flag: '๐Ÿ‡ฌ๐Ÿ‡ญ'}, {name: 'Gibraltar', iso2: CountryISO.Gibraltar, dialCode: '350', flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ'}, {name: 'Greece', iso2: CountryISO.Greece, dialCode: '30', flag: '๐Ÿ‡ฌ๐Ÿ‡ท'}, @@ -380,18 +382,18 @@ export class CountryData { {name: 'Heard Island and McDonald Islands', iso2: CountryISO.HeardIslandandMcDonaldIslands, dialCode: '672', flag: '๐Ÿ‡ญ๐Ÿ‡ฒ'}, {name: 'Honduras', iso2: CountryISO.Honduras, dialCode: '504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ'}, {name: 'Hong Kong', iso2: CountryISO.HongKong, dialCode: '852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ'}, - {name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '๐Ÿ‡ญ๐Ÿ‡บ'}, + {name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '๐Ÿ‡ญ๐Ÿ‡บ', postCodePattern: /[0-9]{4}/}, {name: 'Iceland', iso2: CountryISO.Iceland, dialCode: '354', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ'}, {name: 'India', iso2: CountryISO.India, dialCode: '91', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ'}, {name: 'Indonesia', iso2: CountryISO.Indonesia, dialCode: '62', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ'}, - {name: 'Iran', iso2: CountryISO.Iran, dialCode: '98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท'}, + {name: 'Islamic Republic of Iran', iso2: CountryISO.Iran, dialCode: '98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท'}, {name: 'Iraq', iso2: CountryISO.Iraq, dialCode: '964', flag: '๐Ÿ‡ฎ๐Ÿ‡ถ'}, {name: 'Ireland', iso2: CountryISO.Ireland, dialCode: '353', flag: '๐Ÿ‡ฎ๐Ÿ‡ช'}, {name: 'Isle of Man', iso2: CountryISO.IsleOfMan, dialCode: '44', flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ'}, {name: 'Israel', iso2: CountryISO.Israel, dialCode: '972', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ'}, - {name: 'Italy', iso2: CountryISO.Italy, dialCode: '39', flag: '๐Ÿ‡ฎ๐Ÿ‡น'}, + {name: 'Italy', iso2: CountryISO.Italy, dialCode: '39', flag: '๐Ÿ‡ฎ๐Ÿ‡น', postCodePattern: /[0-9]{5}/}, {name: 'Jamaica', iso2: CountryISO.Jamaica, dialCode: '1', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ'}, - {name: 'Japan', iso2: CountryISO.Japan, dialCode: '81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต'}, + {name: 'Japan', iso2: CountryISO.Japan, dialCode: '81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต', postCodePattern: /\d{3}-\d{4}/}, {name: 'Jersey', iso2: CountryISO.Jersey, dialCode: '44', flag: '๐Ÿ‡ฏ๐Ÿ‡ช'}, {name: 'Jordan', iso2: CountryISO.Jordan, dialCode: '962', flag: '๐Ÿ‡ฏ๐Ÿ‡ด'}, {name: 'Kazakhstan', iso2: CountryISO.Kazakhstan, dialCode: '7', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ'}, @@ -400,7 +402,7 @@ export class CountryData { {name: 'Kosovo', iso2: CountryISO.Kosovo, dialCode: '383', flag: '๐Ÿ‡ฝ๐Ÿ‡ฐ'}, {name: 'Kuwait', iso2: CountryISO.Kuwait, dialCode: '965', flag: '๐Ÿ‡ฐ๐Ÿ‡ผ'}, {name: 'Kyrgyzstan', iso2: CountryISO.Kyrgyzstan, dialCode: '996', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ'}, - {name: 'Laos', iso2: CountryISO.Laos, dialCode: '856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ'}, + {name: 'Lao People\'s Democratic Republic', iso2: CountryISO.Laos, dialCode: '856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ'}, {name: 'Latvia', iso2: CountryISO.Latvia, dialCode: '371', flag: '๐Ÿ‡ฑ๐Ÿ‡ป'}, {name: 'Lebanon', iso2: CountryISO.Lebanon, dialCode: '961', flag: '๐Ÿ‡ฑ๐Ÿ‡ง'}, {name: 'Lesotho', iso2: CountryISO.Lesotho, dialCode: '266', flag: '๐Ÿ‡ฑ๐Ÿ‡ธ'}, @@ -408,9 +410,8 @@ export class CountryData { {name: 'Libya', iso2: CountryISO.Libya, dialCode: '218', flag: '๐Ÿ‡ฑ๐Ÿ‡พ'}, {name: 'Liechtenstein', iso2: CountryISO.Liechtenstein, dialCode: '423', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ'}, {name: 'Lithuania', iso2: CountryISO.Lithuania, dialCode: '370', flag: '๐Ÿ‡ฑ๐Ÿ‡น'}, - {name: 'Luxembourg', iso2: CountryISO.Luxembourg, dialCode: '352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ'}, - {name: 'Macau', iso2: CountryISO.Macau, dialCode: '853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด'}, - {name: 'Macedonia', iso2: CountryISO.Macedonia, dialCode: '389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ'}, + {name: 'Luxembourg', iso2: CountryISO.Luxembourg, dialCode: '352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ', postCodePattern: /(L\s*([-โ€”โ€“]))\s*?\d{4}/}, + {name: 'Macao', iso2: CountryISO.Macau, dialCode: '853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด'}, {name: 'Madagascar', iso2: CountryISO.Madagascar, dialCode: '261', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ'}, {name: 'Malawi', iso2: CountryISO.Malawi, dialCode: '265', flag: '๐Ÿ‡ฒ๐Ÿ‡ผ'}, {name: 'Malaysia', iso2: CountryISO.Malaysia, dialCode: '60', flag: '๐Ÿ‡ฒ๐Ÿ‡พ'}, @@ -423,8 +424,8 @@ export class CountryData { {name: 'Mauritius', iso2: CountryISO.Mauritius, dialCode: '230', flag: '๐Ÿ‡ฒ๐Ÿ‡บ'}, {name: 'Mayotte', iso2: CountryISO.Mayotte, dialCode: '262', flag: '๐Ÿ‡พ๐Ÿ‡น'}, {name: 'Mexico', iso2: CountryISO.Mexico, dialCode: '52', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ'}, - {name: 'Micronesia', iso2: CountryISO.Micronesia, dialCode: '691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ'}, - {name: 'Moldova', iso2: CountryISO.Moldova, dialCode: '373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ'}, + {name: 'Micronesia, Federated States of', iso2: CountryISO.Micronesia, dialCode: '691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ'}, + {name: 'Moldova, Republic of', iso2: CountryISO.Moldova, dialCode: '373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ'}, {name: 'Monaco', iso2: CountryISO.Monaco, dialCode: '377', flag: '๐Ÿ‡ฒ๐Ÿ‡จ'}, {name: 'Mongolia', iso2: CountryISO.Mongolia, dialCode: '976', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ'}, {name: 'Montenegro', iso2: CountryISO.Montenegro, dialCode: '382', flag: '๐Ÿ‡ฒ๐Ÿ‡ช'}, @@ -435,7 +436,7 @@ export class CountryData { {name: 'Namibia', iso2: CountryISO.Namibia, dialCode: '264', flag: '๐Ÿ‡ณ๐Ÿ‡ฆ'}, {name: 'Nauru', iso2: CountryISO.Nauru, dialCode: '674', flag: '๐Ÿ‡ณ๐Ÿ‡ท'}, {name: 'Nepal', iso2: CountryISO.Nepal, dialCode: '977', flag: '๐Ÿ‡ณ๐Ÿ‡ต'}, - {name: 'Netherlands', iso2: CountryISO.Netherlands, dialCode: '31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ'}, + {name: 'Netherlands', iso2: CountryISO.Netherlands, dialCode: '31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ', postCodePattern: /[1-9][0-9]{3}\s?[a-zA-Z]{2}/}, {name: 'New Caledonia', iso2: CountryISO.NewCaledonia, dialCode: '687', flag: '๐Ÿ‡ณ๐Ÿ‡จ'}, {name: 'New Zealand', iso2: CountryISO.NewZealand, dialCode: '64', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ'}, {name: 'Nicaragua', iso2: CountryISO.Nicaragua, dialCode: '505', flag: '๐Ÿ‡ณ๐Ÿ‡ฎ'}, @@ -444,43 +445,44 @@ export class CountryData { {name: 'Niue', iso2: CountryISO.Niue, dialCode: '683', flag: '๐Ÿ‡ณ๐Ÿ‡บ'}, {name: 'Norfolk Island', iso2: CountryISO.NorfolkIsland, dialCode: '672', flag: '๐Ÿ‡ณ๐Ÿ‡ซ'}, {name: 'North Korea', iso2: CountryISO.NorthKorea, dialCode: '850', flag: '๐Ÿ‡ฐ๐Ÿ‡ต'}, + {name: 'North Macedonia', iso2: CountryISO.NorthMacedonia, dialCode: '389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ'}, {name: 'Northern Mariana Islands', iso2: CountryISO.NorthernMarianaIslands, dialCode: '1', flag: '๐Ÿ‡ฒ๐Ÿ‡ต'}, {name: 'Norway', iso2: CountryISO.Norway, dialCode: '47', flag: '๐Ÿ‡ณ๐Ÿ‡ด'}, {name: 'Oman', iso2: CountryISO.Oman, dialCode: '968', flag: '๐Ÿ‡ด๐Ÿ‡ฒ'}, {name: 'Pakistan', iso2: CountryISO.Pakistan, dialCode: '92', flag: '๐Ÿ‡ต๐Ÿ‡ฐ'}, {name: 'Palau', iso2: CountryISO.Palau, dialCode: '680', flag: '๐Ÿ‡ต๐Ÿ‡ผ'}, - {name: 'Palestine', iso2: CountryISO.Palestine, dialCode: '970', flag: '๐Ÿ‡ต๐Ÿ‡ธ'}, + {name: 'Palestine, State of', iso2: CountryISO.Palestine, dialCode: '970', flag: '๐Ÿ‡ต๐Ÿ‡ธ'}, {name: 'Panama', iso2: CountryISO.Panama, dialCode: '507', flag: '๐Ÿ‡ต๐Ÿ‡ฆ'}, {name: 'Papua New Guinea', iso2: CountryISO.PapuaNewGuinea, dialCode: '675', flag: '๐Ÿ‡ต๐Ÿ‡ฌ'}, {name: 'Paraguay', iso2: CountryISO.Paraguay, dialCode: '595', flag: '๐Ÿ‡ต๐Ÿ‡พ'}, {name: 'Peru', iso2: CountryISO.Peru, dialCode: '51', flag: '๐Ÿ‡ต๐Ÿ‡ช'}, {name: 'Philippines', iso2: CountryISO.Philippines, dialCode: '63', flag: '๐Ÿ‡ต๐Ÿ‡ญ'}, {name: 'Pitcairn Islands', iso2: CountryISO.PitcairnIslands, dialCode: '649', flag: '๐Ÿ‡ต๐Ÿ‡ณ'}, - {name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ'}, + {name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ', postCodePattern: /[0-9]{2}-[0-9]{3}/}, {name: 'Portugal', iso2: CountryISO.Portugal, dialCode: '351', flag: '๐Ÿ‡ต๐Ÿ‡น'}, {name: 'Puerto Rico', iso2: CountryISO.PuertoRico, dialCode: '1', flag: '๐Ÿ‡ต๐Ÿ‡ท'}, {name: 'Qatar', iso2: CountryISO.Qatar, dialCode: '974', flag: '๐Ÿ‡ถ๐Ÿ‡ฆ'}, - {name: 'Rรฉunion', iso2: CountryISO.Rรฉunion, dialCode: '262', flag: '๐Ÿ‡ท๐Ÿ‡ช'}, + {name: 'Rรฉunion', iso2: CountryISO.Reunion, dialCode: '262', flag: '๐Ÿ‡ท๐Ÿ‡ช'}, {name: 'Romania', iso2: CountryISO.Romania, dialCode: '40', flag: '๐Ÿ‡ท๐Ÿ‡ด'}, - {name: 'Russia', iso2: CountryISO.Russia, dialCode: '7', flag: '๐Ÿ‡ท๐Ÿ‡บ'}, + {name: 'Russian Federation', iso2: CountryISO.Russia, dialCode: '7', flag: '๐Ÿ‡ท๐Ÿ‡บ'}, {name: 'Rwanda', iso2: CountryISO.Rwanda, dialCode: '250', flag: '๐Ÿ‡ท๐Ÿ‡ผ'}, - {name: 'Saint Barthรฉlemy', iso2: CountryISO.SaintBarthรฉlemy, dialCode: '590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ'}, - {name: 'Saint Helena', iso2: CountryISO.SaintHelena, dialCode: '290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ'}, + {name: 'Saint Barthรฉlemy', iso2: CountryISO.SaintBarthelemy, dialCode: '590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ'}, + {name: 'Saint Helena, Ascension and Tristan da Cunha', iso2: CountryISO.SaintHelena, dialCode: '290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ'}, {name: 'Saint Kitts and Nevis', iso2: CountryISO.SaintKittsAndNevis, dialCode: '1', flag: '๐Ÿ‡ฐ๐Ÿ‡ณ'}, {name: 'Saint Lucia', iso2: CountryISO.SaintLucia, dialCode: '1', flag: '๐Ÿ‡ฑ๐Ÿ‡จ'}, - {name: 'Saint Martin', iso2: CountryISO.SaintMartin, dialCode: '590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ'}, + {name: 'Saint Martin (French part)', iso2: CountryISO.SaintMartin, dialCode: '590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ'}, {name: 'Saint Pierre and Miquelon', iso2: CountryISO.SaintPierreAndMiquelon, dialCode: '508', flag: '๐Ÿ‡ต๐Ÿ‡ฒ'}, {name: 'Saint Vincent and the Grenadines', iso2: CountryISO.SaintVincentAndTheGrenadines, dialCode: '1', flag: '๐Ÿ‡ป๐Ÿ‡จ'}, {name: 'Samoa', iso2: CountryISO.Samoa, dialCode: '685', flag: '๐Ÿ‡ผ๐Ÿ‡ธ'}, {name: 'San Marino', iso2: CountryISO.SanMarino, dialCode: '378', flag: '๐Ÿ‡ธ๐Ÿ‡ฒ'}, - {name: 'Sรฃo Tomรฉ and Prรญncipe', iso2: CountryISO.SรฃoTomรฉAndPrรญncipe, dialCode: '239', flag: '๐Ÿ‡ธ๐Ÿ‡น'}, + {name: 'Sao Tome and Principe', iso2: CountryISO.SaoTomeAndPrincipe, dialCode: '239', flag: '๐Ÿ‡ธ๐Ÿ‡น'}, {name: 'Saudi Arabia', iso2: CountryISO.SaudiArabia, dialCode: '966', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ'}, {name: 'Senegal', iso2: CountryISO.Senegal, dialCode: '221', flag: '๐Ÿ‡ธ๐Ÿ‡ณ'}, {name: 'Serbia', iso2: CountryISO.Serbia, dialCode: '381', flag: '๐Ÿ‡ท๐Ÿ‡ธ'}, {name: 'Seychelles', iso2: CountryISO.Seychelles, dialCode: '248', flag: '๐Ÿ‡ธ๐Ÿ‡จ'}, {name: 'Sierra Leone', iso2: CountryISO.SierraLeone, dialCode: '232', flag: '๐Ÿ‡ธ๐Ÿ‡ฑ'}, {name: 'Singapore', iso2: CountryISO.Singapore, dialCode: '65', flag: '๐Ÿ‡ธ๐Ÿ‡ฌ'}, - {name: 'Sint Maarten', iso2: CountryISO.SintMaarten, dialCode: '1', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ'}, + {name: 'Sint Maarten (Dutch Part)', iso2: CountryISO.SintMaarten, dialCode: '1', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ'}, {name: 'Slovakia', iso2: CountryISO.Slovakia, dialCode: '421', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ'}, {name: 'Slovenia', iso2: CountryISO.Slovenia, dialCode: '386', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ'}, {name: 'Solomon Islands', iso2: CountryISO.SolomonIslands, dialCode: '677', flag: '๐Ÿ‡ธ๐Ÿ‡ง'}, @@ -490,18 +492,18 @@ export class CountryData { iso2: CountryISO.SouthGeorgiaAndTheSouthSandwichIslands, dialCode: '500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ'}, {name: 'South Korea', iso2: CountryISO.SouthKorea, dialCode: '82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท'}, {name: 'South Sudan', iso2: CountryISO.SouthSudan, dialCode: '211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ'}, - {name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '๐Ÿ‡ช๐Ÿ‡ธ'}, + {name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '๐Ÿ‡ช๐Ÿ‡ธ', postCodePattern: /((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}/}, {name: 'Sri Lanka', iso2: CountryISO.SriLanka, dialCode: '94', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ'}, {name: 'Sudan', iso2: CountryISO.Sudan, dialCode: '249', flag: '๐Ÿ‡ธ๐Ÿ‡ฉ'}, - {name: 'Suriname: ', iso2: CountryISO.Suriname, dialCode: '597', flag: '๐Ÿ‡ธ๐Ÿ‡ท'}, + {name: 'Suriname', iso2: CountryISO.Suriname, dialCode: '597', flag: '๐Ÿ‡ธ๐Ÿ‡ท'}, {name: 'Svalbard and Jan Mayen', iso2: CountryISO.SvalbardAndJanMayen, dialCode: '47', flag: '๐Ÿ‡ธ๐Ÿ‡ฏ'}, {name: 'Swaziland', iso2: CountryISO.Swaziland, dialCode: '268', flag: '๐Ÿ‡ธ๐Ÿ‡ฟ'}, - {name: 'Sweden', iso2: CountryISO.Sweden, dialCode: '46', flag: '๐Ÿ‡ธ๐Ÿ‡ช'}, + {name: 'Sweden', iso2: CountryISO.Sweden, dialCode: '46', flag: '๐Ÿ‡ธ๐Ÿ‡ช', postCodePattern: /\d{3}\s?\d{2}/}, {name: 'Switzerland', iso2: CountryISO.Switzerland, dialCode: '41', flag: '๐Ÿ‡จ๐Ÿ‡ญ'}, - {name: 'Syria', iso2: CountryISO.Syria, dialCode: '963', flag: '๐Ÿ‡ธ๐Ÿ‡พ'}, + {name: 'Syrian Arab Republic', iso2: CountryISO.Syria, dialCode: '963', flag: '๐Ÿ‡ธ๐Ÿ‡พ'}, {name: 'Taiwan', iso2: CountryISO.Taiwan, dialCode: '886', flag: '๐Ÿ‡น๐Ÿ‡ผ'}, {name: 'Tajikistan', iso2: CountryISO.Tajikistan, dialCode: '992', flag: '๐Ÿ‡น๐Ÿ‡ฏ'}, - {name: 'Tanzania', iso2: CountryISO.Tanzania, dialCode: '255', flag: '๐Ÿ‡น๐Ÿ‡ฟ'}, + {name: 'Tanzania, United Republic of', iso2: CountryISO.Tanzania, dialCode: '255', flag: '๐Ÿ‡น๐Ÿ‡ฟ'}, {name: 'Thailand', iso2: CountryISO.Thailand, dialCode: '66', flag: '๐Ÿ‡น๐Ÿ‡ญ'}, {name: 'Timor-Leste', iso2: CountryISO.TimorLeste, dialCode: '670', flag: '๐Ÿ‡น๐Ÿ‡ฑ'}, {name: 'Togo', iso2: CountryISO.Togo, dialCode: '228', flag: '๐Ÿ‡น๐Ÿ‡ฌ'}, @@ -517,8 +519,8 @@ export class CountryData { {name: 'Uganda', iso2: CountryISO.Uganda, dialCode: '256', flag: '๐Ÿ‡บ๐Ÿ‡ฌ'}, {name: 'Ukraine', iso2: CountryISO.Ukraine, dialCode: '380', flag: '๐Ÿ‡บ๐Ÿ‡ฆ'}, {name: 'United Arab Emirates', iso2: CountryISO.UnitedArabEmirates, dialCode: '971', flag: '๐Ÿ‡ฆ๐Ÿ‡ช'}, - {name: 'United Kingdom', iso2: CountryISO.UnitedKingdom, dialCode: '44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง'}, - {name: 'United States', iso2: CountryISO.UnitedStates, dialCode: '1', flag: '๐Ÿ‡บ๐Ÿ‡ธ'}, + {name: 'United Kingdom', iso2: CountryISO.UnitedKingdom, dialCode: '44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง', postCodePattern: /[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}/}, + {name: 'United States', iso2: CountryISO.UnitedStates, dialCode: '1', flag: '๐Ÿ‡บ๐Ÿ‡ธ', postCodePattern: /(\d{5}(-\d{4})?)/}, {name: 'Uruguay', iso2: CountryISO.Uruguay, dialCode: '598', flag: '๐Ÿ‡บ๐Ÿ‡พ'}, {name: 'Uzbekistan', iso2: CountryISO.Uzbekistan, dialCode: '998', flag: '๐Ÿ‡บ๐Ÿ‡ฟ'}, {name: 'Vanuatu', iso2: CountryISO.Vanuatu, dialCode: '678', flag: '๐Ÿ‡ป๐Ÿ‡บ'}, @@ -530,6 +532,7 @@ export class CountryData { {name: 'Yemen', iso2: CountryISO.Yemen, dialCode: '967', flag: '๐Ÿ‡พ๐Ÿ‡ช'}, {name: 'Zambia', iso2: CountryISO.Zambia, dialCode: '260', flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ'}, {name: 'Zimbabwe', iso2: CountryISO.Zimbabwe, dialCode: '263', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ'}, - {name: 'ร…land Islands', iso2: CountryISO.ร…landIslands, dialCode: '358', flag: '๐Ÿ‡ฆ๐Ÿ‡ฝ'} + {name: 'ร…land Islands', iso2: CountryISO.AlandIslands, dialCode: '358', flag: '๐Ÿ‡ฆ๐Ÿ‡ฝ'} ]; } +/* eslint-enable max-len */ diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 3d4edacd1d..21c912edf5 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -222,6 +222,7 @@ import { WidgetButtonComponent } from '@shared/components/button/widget-button.c import { HexInputComponent } from '@shared/components/color-picker/hex-input.component'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component'; +import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -383,6 +384,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TogglePasswordComponent, ProtobufContentComponent, BranchAutocompleteComponent, + CountryAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, NotificationComponent, @@ -640,6 +642,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TogglePasswordComponent, ProtobufContentComponent, BranchAutocompleteComponent, + CountryAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, NotificationComponent, 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 d9259eec78..26c955c6ea 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -992,6 +992,7 @@ }, "contact": { "country": "Country", + "country-required": "Country is required.", "city": "City", "state": "State / Province", "postal-code": "Zip / Postal Code", @@ -1001,6 +1002,8 @@ "phone": "Phone", "email": "Email", "no-address": "No address", + "no-country-found": "No countries found.", + "no-country-matching": "No country matching '{{country}}' were found.", "state-max-length": "State length should be less than 256", "phone-max-length": "Phone number should be less than 256", "city-max-length": "Specified city should be less than 256" From 0af0c3667378be8ba180786ae121e5b11d8ff084 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 2 Sep 2024 10:41:17 +0200 Subject: [PATCH 319/331] refactoring --- .../thingsboard/server/service/ws/WebSocketSessionRef.java | 5 +++++ .../ws/notification/DefaultNotificationCommandsHandler.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java index 44919b138c..7ce7bd5735 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.ws; import lombok.Builder; import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.security.model.SecurityUser; import java.net.InetSocketAddress; @@ -39,6 +40,10 @@ public class WebSocketSessionRef { private final WebSocketSessionType sessionType; private final AtomicInteger sessionSubIdSeq = new AtomicInteger(); + public TenantId getTenantId() { + return securityCtx != null ? securityCtx.getTenantId() : TenantId.SYS_TENANT_ID; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index 84f3698845..77f000990c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -242,7 +242,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH @Override public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { - localSubscriptionService.cancelSubscription(sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSessionId(), cmd.getCmdId()); + localSubscriptionService.cancelSubscription(sessionRef.getTenantId(), sessionRef.getSessionId(), cmd.getCmdId()); } private void sendUpdate(String sessionId, CmdUpdate update) { From 73ae4b3c666d3bc257276a5354dacd5d0df2ee72 Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Mon, 2 Sep 2024 12:24:58 +0300 Subject: [PATCH 320/331] UI: Image service - change parameters order to keep backward compatibility with older version --- ui-ngx/src/app/core/http/image.service.ts | 4 ++-- .../app/shared/components/image/image-gallery.component.ts | 2 +- ui-ngx/src/app/shared/components/image/images-datasource.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts index 68c128028f..835e8ffb61 100644 --- a/ui-ngx/src/app/core/http/image.service.ts +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -87,8 +87,8 @@ export class ImageService { imageInfo, defaultHttpOptionsFromConfig(config)); } - public getImages(pageLink: PageLink, imageSubType: ResourceSubType = ResourceSubType.IMAGE, - includeSystemImages = false, config?: RequestConfig): Observable> { + public getImages(pageLink: PageLink, includeSystemImages = false, + imageSubType: ResourceSubType = ResourceSubType.IMAGE, config?: RequestConfig): Observable> { return this.http.get>( `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`, defaultHttpOptionsFromConfig(config)); diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts index 5b50282ea7..23f489d78c 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -205,7 +205,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe property: 'createdTime', direction: Direction.DESC }); - return this.imageService.getImages(pageLink, this.imageSubType, filter.includeSystemImages); + return this.imageService.getImages(pageLink, filter.includeSystemImages, this.imageSubType); }; } diff --git a/ui-ngx/src/app/shared/components/image/images-datasource.ts b/ui-ngx/src/app/shared/components/image/images-datasource.ts index 3a46508d45..78f02df4b1 100644 --- a/ui-ngx/src/app/shared/components/image/images-datasource.ts +++ b/ui-ngx/src/app/shared/components/image/images-datasource.ts @@ -84,7 +84,7 @@ export class ImagesDatasource implements DataSource { } fetchEntities(pageLink: PageLink, imageSubType: ResourceSubType, includeSystemImages = false): Observable> { - return this.imageService.getImages(pageLink, imageSubType, includeSystemImages); + return this.imageService.getImages(pageLink, includeSystemImages, imageSubType); } isAllSelected(): Observable { From ff01f77f9e60c47dac7af090a7897d8c746943b7 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 2 Sep 2024 15:27:42 +0300 Subject: [PATCH 321/331] UI: Added additionEntityTypes in entity-select.component and delete CURRENT_RULE_NODE --- .../entity/entity-autocomplete.component.ts | 2 - .../entity/entity-select.component.html | 3 +- .../entity/entity-select.component.ts | 24 ++++++----- .../entity/entity-type-select.component.ts | 40 +++++++++---------- .../app/shared/models/entity-type.models.ts | 10 +---- .../assets/locale/locale.constant-en_US.json | 1 - 6 files changed, 36 insertions(+), 44 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 071559f234..b55f01a7ae 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -414,8 +414,6 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } else { return EntityType.CUSTOMER; } - } else if (entityType === AliasEntityType.CURRENT_RULE_NODE) { - return EntityType.RULE_NODE; } return entityType; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index b4a3a33029..7f5c127e00 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -22,10 +22,11 @@ [required]="required" [useAliasEntityTypes]="useAliasEntityTypes" [allowedEntityTypes]="allowedEntityTypes" + [additionEntityTypes]="additionEntityTypes" formControlName="entityType"> diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index eff7f5306f..5671e9de3b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -22,8 +22,8 @@ import { TranslateService } from '@ngx-translate/core'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { EntityService } from '@core/http/entity.service'; import { EntityId } from '@shared/models/id/entity-id'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-entity-select', @@ -47,24 +47,22 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @Input() useAliasEntityTypes: boolean; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() disabled: boolean; + @Input() + additionEntityTypes: {[entityType in string]: string} = {}; + displayEntityTypeSelect: boolean; AliasEntityType = AliasEntityType; - entityTypeNullUid: Set = new Set([ - AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER, AliasEntityType.CURRENT_RULE_NODE + entityTypeNullUUID: Set = new Set([ + AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER ]); private readonly defaultEntityType: EntityType | AliasEntityType = null; @@ -110,6 +108,10 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte this.updateView(this.modelValue.entityType, id); } ); + const additionNullUIIDEntityTypes = Object.keys(this.additionEntityTypes) as string[]; + if (additionNullUIIDEntityTypes.length > 0) { + additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType)); + } } ngAfterViewInit(): void { @@ -147,7 +149,7 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte id: this.modelValue.entityType !== entityType ? null : entityId }; - if (this.entityTypeNullUid.has(this.modelValue.entityType)) { + if (this.entityTypeNullUUID.has(this.modelValue.entityType)) { this.modelValue.id = NULL_UUID; } else if (this.modelValue.entityType === AliasEntityType.CURRENT_CUSTOMER && !this.modelValue.id) { this.modelValue.id = NULL_UUID; diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index 3b0dca6e81..2405a269b6 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -15,13 +15,13 @@ /// import { AfterViewInit, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models'; import { EntityService } from '@core/http/entity.service'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-entity-type-select', @@ -48,28 +48,21 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @Input() filterAllowedEntityTypes = true; - private showLabelValue: boolean; - get showLabel(): boolean { - return this.showLabelValue; - } @Input() - set showLabel(value: boolean) { - this.showLabelValue = coerceBooleanProperty(value); - } + @coerceBoolean() + showLabel: boolean; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() disabled: boolean; - entityTypes: Array; + @Input() + additionEntityTypes: {[key in string]: string} = {}; + + entityTypes: Array; private propagateChange = (v: any) => { }; @@ -93,8 +86,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, this.entityTypes = this.filterAllowedEntityTypes ? this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes; - if (this.allowedEntityTypes?.length && this.allowedEntityTypes.includes(AliasEntityType.CURRENT_RULE_NODE)) { - this.entityTypes.push(AliasEntityType.CURRENT_RULE_NODE); + const additionEntityTypes = Object.keys(this.additionEntityTypes); + if (additionEntityTypes.length > 0) { + this.entityTypes.push(...additionEntityTypes); } this.entityTypeFormGroup.get('entityType').valueChanges.subscribe( (value) => { @@ -116,6 +110,10 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, if (propName === 'allowedEntityTypes') { this.entityTypes = this.filterAllowedEntityTypes ? this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes; + const additionEntityTypes = Object.keys(this.additionEntityTypes); + if (additionEntityTypes.length > 0) { + this.entityTypes.push(...additionEntityTypes); + } const currentEntityType: EntityType | AliasEntityType = this.entityTypeFormGroup.get('entityType').value; if (currentEntityType && !this.entityTypes.includes(currentEntityType)) { this.entityTypeFormGroup.get('entityType').patchValue(null, {emitEvent: true}); @@ -155,7 +153,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, } displayEntityTypeFn(entityType?: EntityType | AliasEntityType | null): string | undefined { - if (entityType) { + if (this.additionEntityTypes[entityType]) { + return this.additionEntityTypes[entityType]; + } else if (entityType) { return this.translate.instant(entityTypeTranslations.get(entityType as EntityType).type); } else { return ''; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index f2467b9736..51fefefdf2 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -54,8 +54,7 @@ export enum AliasEntityType { CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', CURRENT_TENANT = 'CURRENT_TENANT', CURRENT_USER = 'CURRENT_USER', - CURRENT_USER_OWNER = 'CURRENT_USER_OWNER', - CURRENT_RULE_NODE = 'CURRENT_RULE_NODE' + CURRENT_USER_OWNER = 'CURRENT_USER_OWNER' } export interface EntityTypeTranslation { @@ -327,13 +326,6 @@ export const entityTypeTranslations = new Map Date: Mon, 2 Sep 2024 15:32:00 +0300 Subject: [PATCH 322/331] UI: Clear code --- .../app/shared/components/entity/entity-select.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index 7f5c127e00..676294f344 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
Date: Mon, 2 Sep 2024 16:46:04 +0300 Subject: [PATCH 323/331] translation small fix --- .../basic/gateway-basic-configuration.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html index b515d1baeb..9c67bd2eaf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html @@ -725,7 +725,7 @@ - {{ 'gateway.min-pack-send-delay-min-pattern' | translate }} + {{ 'gateway.min-pack-send-delay-pattern' | translate }} info_outlined From 5c483284235d34489109f266bc91473a0a8d33f1 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 3 Sep 2024 12:13:31 +0200 Subject: [PATCH 324/331] fixed DefaultTbLocalSubscriptionService build --- .../DefaultTbLocalSubscriptionService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index f82f48bf74..bbfdd2d44f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -61,6 +61,7 @@ import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -158,28 +159,29 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer * Even if we cache locally the list of active subscriptions by entity id, it is still time-consuming operation to get them from cache * Since number of subscriptions is usually much less than number of devices that are pushing data. */ - Set staleSubs = new HashSet<>(); + Map> staleSubs = new HashMap<>(); subscriptionsByEntityId.forEach((id, sub) -> { try { pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED)); } catch (TenantNotFoundException e) { - staleSubs.add(id); + staleSubs.computeIfAbsent(sub.getTenantId(), key -> new HashSet<>()).add(id); log.warn("Cleaning up stale subscription {} for tenant {} due to TenantNotFoundException", id, sub.getTenantId()); } catch (Exception e) { log.error("Failed to push subscription {} to manager service", sub, e); } }); - if (!staleSubs.isEmpty()) { + staleSubs.forEach((tenantId, subs) -> { + var subsLock = getSubsLock(tenantId); subsLock.lock(); try { - staleSubs.forEach(entityId -> { + subs.forEach(entityId -> { subscriptionsByEntityId.remove(entityId); entityUpdates.remove(entityId); }); } finally { subsLock.unlock(); } - } + }); } } From 3a6a44a9a4567ed97bada32c182193c7405e792d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 3 Sep 2024 13:42:28 +0300 Subject: [PATCH 325/331] Refactoring after review --- .../service/queue/DefaultTbCoreConsumerService.java | 2 +- .../service/queue/DefaultTbEdgeConsumerService.java | 12 +++++------- .../server/service/queue/TbEdgeConsumerService.java | 6 +----- application/src/main/resources/thingsboard.yml | 2 ++ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index d30f03262d..2a64dd3388 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -243,7 +243,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException { @@ -235,7 +233,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService { - +public interface TbEdgeConsumerService { } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9de3d88568..aefb78106d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1798,6 +1798,8 @@ queue: pack-processing-timeout: "${TB_QUEUE_EDGE_PACK_PROCESSING_TIMEOUT_MS:10000}" # Retries for processing a failure message pack by Edge services pack-processing-retries: "${TB_QUEUE_EDGE_MESSAGE_PROCESSING_RETRIES:3}" + # Enable/disable a separate consumer per partition for Edge queue + consumer-per-partition: "${TB_QUEUE_EDGE_CONSUMER_PER_PARTITION:false}" stats: # Enable/disable statistics for Edge services enabled: "${TB_QUEUE_EDGE_STATS_ENABLED:true}" From 977c36d83f123a5a1426ea9eb9b99f1de180875e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:04 +0300 Subject: [PATCH 326/331] Fix RelatedEdgesCacheValue --- .../server/cache/edge/RelatedEdgesCacheValue.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java index a46b2c5875..1289d09353 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.cache.edge; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.page.PageData; @@ -23,15 +25,13 @@ import java.io.Serial; import java.io.Serializable; @Data +@NoArgsConstructor +@AllArgsConstructor public class RelatedEdgesCacheValue implements Serializable { @Serial private static final long serialVersionUID = -2765080094748518572L; - private final PageData pageData; - - public RelatedEdgesCacheValue(PageData pageData) { - this.pageData = pageData; - } + private PageData pageData; } From 591cc91733fd491ffb54fa5605f0e8ea7805f44f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:23 +0300 Subject: [PATCH 327/331] Fix created edge version --- .../server/service/entitiy/edge/DefaultTbEdgeService.java | 2 +- .../java/org/thingsboard/server/common/data/edge/Edge.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index 5b8fd949bd..8771b48c35 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -54,7 +54,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE if (ActionType.ADDED.equals(actionType)) { ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId); - edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + savedEdge = edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index ec8ade7f69..34ba77269d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -75,9 +75,9 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel this.tenantId = edge.getTenantId(); this.customerId = edge.getCustomerId(); this.rootRuleChainId = edge.getRootRuleChainId(); + this.name = edge.getName(); this.type = edge.getType(); this.label = edge.getLabel(); - this.name = edge.getName(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); this.version = edge.getVersion(); @@ -87,9 +87,9 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel this.tenantId = edge.getTenantId(); this.customerId = edge.getCustomerId(); this.rootRuleChainId = edge.getRootRuleChainId(); + this.name = edge.getName(); this.type = edge.getType(); this.label = edge.getLabel(); - this.name = edge.getName(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); this.version = edge.getVersion(); From 937621ea43effb75bb4e3866d23ee2623b7c28cb Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:31 +0300 Subject: [PATCH 328/331] Fix testDeviceVc_sameTenant --- .../thingsboard/server/service/sync/vc/VersionControlTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index ffc88f7f5a..2e01d700a9 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -241,7 +241,7 @@ public class VersionControlTest extends AbstractControllerTest { checkImportedEntity(tenantId1, device, tenantId1, importedDevice); assertThat(importedDevice.getDeviceProfileId()).isEqualTo(device.getDeviceProfileId()); - assertThat(findDeviceCredentials(device.getId())).isEqualTo(deviceCredentials); + assertThat(findDeviceCredentials(device.getId())).isEqualToIgnoringGivenFields(deviceCredentials, "version"); assertThat(importedDevice.getFirmwareId()).isEqualTo(firmware.getId()); assertThat(importedDevice.getSoftwareId()).isEqualTo(software.getId()); } From 6d109bdcf236404877e86c38defc45be6867eccb Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 3 Sep 2024 18:05:51 +0300 Subject: [PATCH 329/331] UI: Reset css class tb-suffix-absolute in dashboard-settings-dialog --- .../dashboard-settings-dialog.component.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 1c3ffbc922..0a1733e851 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -145,8 +145,7 @@
dashboard.min-layout-width
- + @@ -191,7 +190,7 @@
dashboard.row-height
- + @@ -246,7 +245,7 @@
dashboard.mobile-row-height
- + From 9c961878ebb942b9e1c90d6b5afbcc0162a4d122 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 3 Sep 2024 18:15:21 +0300 Subject: [PATCH 330/331] UI: Fixed process entity type in entity table widgets --- .../lib/entity/entities-table-widget.component.ts | 10 ++++++---- .../home/components/widget/lib/table-widget.models.ts | 3 ++- .../en_US/widget/action/show_widget_action_cell_fn.md | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index c6c07fb1ad..ab25a96af1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -49,7 +49,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { BehaviorSubject, fromEvent, merge, Observable, Subject } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, SortDirection } from '@angular/material/sort'; @@ -408,7 +408,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } as EntityColumn ); this.contentsInfo.entityType = { - useCellContentFunction: false + useCellContentFunction: true, + cellContentFunction: (entityType: EntityType) => + entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : '' }; this.stylesInfo.entityType = { useCellStyleFunction: false @@ -869,9 +871,9 @@ class EntityDatasource implements DataSource { } if (datasource.entityType) { entity.id.entityType = datasource.entityType; - entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type); + entity.entityType = datasource.entityType; } else { - entity.entityType = ''; + entity.entityType = null; } this.dataKeys.forEach((dataKey, index) => { const keyData = data[index].data; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 33428e949d..dcb40cd85d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -25,6 +25,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; +import { EntityType } from '@shared/models/entity-type.models'; type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; @@ -65,7 +66,7 @@ export interface EntityData { id: EntityId; entityName: string; entityLabel?: string; - entityType?: string; + entityType?: EntityType; actionCellButtons?: TableCellButtonActionDescriptor[]; hasActions?: boolean; [key: string]: any; diff --git a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md index 8708993103..459a71e8cc 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md @@ -36,7 +36,7 @@ return widgetContext.currentUser.authority === 'CUSTOMER_USER'; * Display action only if the entity in the row is device and has type `thermostat`: ```javascript -return data && data.entityType === 'Device' && data.Type === 'thermostat'; +return data && data.entityType === 'DEVICE' && data.Type === 'thermostat'; {:copy-code} ``` From bf6bd064b7e4eb3fb82bf0db820a4616dd0aad2b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 4 Sep 2024 12:01:13 +0300 Subject: [PATCH 331/331] UI: Add country data to module providers. --- ui-ngx/src/app/shared/shared.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 21c912edf5..0ed819b248 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -223,6 +223,7 @@ import { HexInputComponent } from '@shared/components/color-picker/hex-input.com import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component'; import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component'; +import { CountryData } from '@shared/models/country.models'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -279,7 +280,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) disableTooltipInteractivity: true } }, - TbBreakPointsProvider + TbBreakPointsProvider, + CountryData ], declarations: [ FooterComponent,