From f079563daf486e0be4ef25998540dcb02f2be092 Mon Sep 17 00:00:00 2001 From: Philip Magyar Date: Thu, 22 Feb 2024 18:10:06 +0100 Subject: [PATCH 01/30] 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 02/30] 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 cbe5150eda97834cf2587b2d271793509c25e403 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 14:44:20 +0300 Subject: [PATCH 03/30] 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 7e7656f7b439b6b648640a2ec4523849b40b8b39 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 16:32:09 +0300 Subject: [PATCH 04/30] 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 05/30] 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 06/30] 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 b56f75a99675a110b6efe23fd17162c02e108b98 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 29 Aug 2024 10:45:34 +0300 Subject: [PATCH 07/30] added validation on init --- .../engine/aws/lambda/TbAwsLambdaNode.java | 25 ++++++- .../aws/lambda/TbAwsLambdaNodeTest.java | 72 +++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 2512125ff1..d20ff2f75c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -62,9 +62,7 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); - if (StringUtils.isBlank(config.getFunctionName())) { - throw new TbNodeException("Function name must be set!", true); - } + validateConfig(); try { AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() @@ -138,6 +136,27 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } + private void validateConfig() throws TbNodeException { + if (StringUtils.isBlank(config.getFunctionName())) { + throw new TbNodeException("Function name must be set!", true); + } + if (StringUtils.isBlank(config.getAccessKey())) { + throw new TbNodeException("Access Key must be set!", true); + } + if (StringUtils.isBlank(config.getSecretKey())) { + throw new TbNodeException("Secret Access Key must be set!", true); + } + if (StringUtils.isBlank(config.getRegion())) { + throw new TbNodeException("Region must be set!", true); + } + if (config.getConnectionTimeout() < 0) { + throw new TbNodeException("Min connection timeout is 0!", true); + } + if (config.getRequestTimeout() < 0) { + throw new TbNodeException("Min request timeout is 0!", true); + } + } + @Override public void destroy() { if (client != null) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java index 4ee954991c..eed3613a48 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java @@ -75,10 +75,14 @@ public class TbAwsLambdaNodeTest { void setUp() { node = new TbAwsLambdaNode(); config = new TbAwsLambdaNodeConfiguration().defaultConfiguration(); + config.setAccessKey("accessKey"); + config.setSecretKey("secretKey"); + config.setFunctionName("new-function"); } @Test public void verifyDefaultConfig() { + config = new TbAwsLambdaNodeConfiguration().defaultConfiguration(); assertThat(config.getAccessKey()).isNull(); assertThat(config.getSecretKey()).isNull(); assertThat(config.getRegion()).isEqualTo(("us-east-1")); @@ -97,7 +101,70 @@ public class TbAwsLambdaNodeTest { var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); assertThatThrownBy(() -> node.init(ctx, configuration)) .isInstanceOf(TbNodeException.class) - .hasMessage("Function name must be set!"); + .hasMessage("Function name must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { + config.setAccessKey(accessKey); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Access Key must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { + config.setSecretKey(secretAccessKey); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Secret Access Key must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidRegion_whenInit_thenThrowsException(String region) { + config.setRegion(region); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Region must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { + config.setConnectionTimeout(-100); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Min connection timeout is 0!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { + config.setRequestTimeout(-100); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Min request timeout is 0!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } @ParameterizedTest @@ -281,9 +348,6 @@ public class TbAwsLambdaNodeTest { } private void init() { - config.setAccessKey("accessKey"); - config.setSecretKey("secretKey"); - config.setFunctionName("new-function"); ReflectionTestUtils.setField(node, "client", clientMock); ReflectionTestUtils.setField(node, "config", config); } From 5256cae80406a06842cff423a2ecdfe2361fe2b0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 29 Aug 2024 12:35:30 +0300 Subject: [PATCH 08/30] used jakarta validation constraints --- .../engine/aws/lambda/TbAwsLambdaNode.java | 26 ++------ .../lambda/TbAwsLambdaNodeConfiguration.java | 8 +++ .../aws/lambda/TbAwsLambdaNodeTest.java | 59 +++++++------------ 3 files changed, 32 insertions(+), 61 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index d20ff2f75c..06dc0310ae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -39,6 +39,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -62,7 +64,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); - validateConfig(); + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + validateFields(config, errorPrefix); try { AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() @@ -136,27 +139,6 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } - private void validateConfig() throws TbNodeException { - if (StringUtils.isBlank(config.getFunctionName())) { - throw new TbNodeException("Function name must be set!", true); - } - if (StringUtils.isBlank(config.getAccessKey())) { - throw new TbNodeException("Access Key must be set!", true); - } - if (StringUtils.isBlank(config.getSecretKey())) { - throw new TbNodeException("Secret Access Key must be set!", true); - } - if (StringUtils.isBlank(config.getRegion())) { - throw new TbNodeException("Region must be set!", true); - } - if (config.getConnectionTimeout() < 0) { - throw new TbNodeException("Min connection timeout is 0!", true); - } - if (config.getRequestTimeout() < 0) { - throw new TbNodeException("Min request timeout is 0!", true); - } - } - @Override public void destroy() { if (client != null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java index c7c66599d5..b395f6b15e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.aws.lambda; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -23,12 +25,18 @@ public class TbAwsLambdaNodeConfiguration implements NodeConfiguration node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Function name must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -111,12 +107,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { config.setAccessKey(accessKey); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Access Key must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -124,12 +115,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { config.setSecretKey(secretAccessKey); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Secret Access Key must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -137,34 +123,19 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidRegion_whenInit_thenThrowsException(String region) { config.setRegion(region); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Region must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @Test public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { config.setConnectionTimeout(-100); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Min connection timeout is 0!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @Test public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { config.setRequestTimeout(-100); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Min request timeout is 0!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -347,6 +318,16 @@ public class TbAwsLambdaNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg); } + private void verifyDataValidationExceptionOnInit() { + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("test"); + when(ctx.getSelf()).thenReturn(ruleNode); + String errorPrefix = "'test' node configuration is invalid: "; + assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining(errorPrefix); + } + private void init() { ReflectionTestUtils.setField(node, "client", clientMock); ReflectionTestUtils.setField(node, "config", config); From d6c40c1bb20ce4b44e9575fdb4676bbe5060645d Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Thu, 29 Aug 2024 18:28:36 +0300 Subject: [PATCH 09/30] 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 73ae4b3c666d3bc257276a5354dacd5d0df2ee72 Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Mon, 2 Sep 2024 12:24:58 +0300 Subject: [PATCH 16/30] 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 60f89ba47354c13bc380e04507d38df3a31f549d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 13:23:13 +0300 Subject: [PATCH 17/30] added validation for client id length --- .../rule/engine/mqtt/TbMqttNode.java | 16 +++++++-- .../rule/engine/mqtt/TbMqttNodeTest.java | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index c909a44a27..5d9c9548ad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -77,6 +77,8 @@ public class TbMqttNode extends TbAbstractExternalNode { this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); try { this.mqttClient = initClient(ctx); + } catch (TbNodeException e) { + throw e; } catch (Exception e) { throw new TbNodeException(e); } @@ -119,8 +121,7 @@ public class TbMqttNode extends TbAbstractExternalNode { MqttClientConfig config = new MqttClientConfig(getSslContext()); config.setOwnerId(getOwnerId(ctx)); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { - config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? - this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); + config.setClientId(getClientId(ctx)); } config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); @@ -146,6 +147,17 @@ public class TbMqttNode extends TbAbstractExternalNode { return client; } + private String getClientId(TbContext ctx) throws TbNodeException { + String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ? + this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : + this.mqttNodeConfiguration.getClientId(); + if (clientId.length() > 23) { + throw new TbNodeException("Client ID was too long '" + clientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); + } + return clientId; + } + MqttClient getMqttClient(TbContext ctx, MqttClientConfig config) { return MqttClient.create(config, null, ctx.getExternalCallExecutor()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index da568eefbb..90ecba923b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -191,6 +191,42 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))); } + @Test + public void givenClientIdIsTooLong_whenInit_thenThrowsException() { + String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj"; + mqttNodeConfig.setClientId(invalidClientId); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Client ID was too long '" + invalidClientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() { + String validClientId = "fertjnhnjj4ge"; + mqttNodeConfig.setClientId("fertjnhnjj4ge"); + mqttNodeConfig.setAppendClientIdSuffix(true); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + String serviceId = "test-service"; + given(ctxMock.getServiceId()).willReturn(serviceId); + + String resultedClientId = validClientId + "_" + serviceId; + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Client ID was too long '" + resultedClientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + @Test public void givenFailedByTimeoutConnectResult_whenInit_thenThrowsException() throws ExecutionException, InterruptedException, TimeoutException { mqttNodeConfig.setHost("localhost"); From ea5519893bfa16b4d1b56fab8a1137cc5012763a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 14:09:03 +0300 Subject: [PATCH 18/30] fixed error message --- .../main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 5d9c9548ad..912496f39e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -152,7 +152,7 @@ public class TbMqttNode extends TbAbstractExternalNode { this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId(); if (clientId.length() > 23) { - throw new TbNodeException("Client ID was too long '" + clientId + "'. " + + throw new TbNodeException("Client ID is too long '" + clientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); } return clientId; From 977c36d83f123a5a1426ea9eb9b99f1de180875e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:04 +0300 Subject: [PATCH 19/30] 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 20/30] 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 21/30] 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 b518a1168eb6bdcef6d34974383339ec2c1399af Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 3 Sep 2024 17:08:24 +0300 Subject: [PATCH 22/30] rollback to old api path for backward compatibility --- .../org/thingsboard/server/controller/OAuth2Controller.java | 2 +- ui-ngx/src/app/core/auth/auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 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 261c4705a9..8a8055c4af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -75,7 +75,7 @@ public class OAuth2Controller extends BaseController { @ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " + "to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " + "the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)") - @PostMapping(value = "/noauth/oauth2/client") + @PostMapping(value = "/noauth/oauth2Clients") public List getOAuth2Clients(HttpServletRequest request, @Parameter(description = "Mobile application package name, to find OAuth2 clients " + "where there is configured mobile application with such package name") diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 6f1c2bc116..5f8a968710 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -224,7 +224,7 @@ export class AuthService { } public loadOAuth2Clients(): Observable> { - const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB; + const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB; return this.http.post>(url, null, defaultHttpOptions()).pipe( catchError(err => of([])), From 6d109bdcf236404877e86c38defc45be6867eccb Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 3 Sep 2024 18:05:51 +0300 Subject: [PATCH 23/30] 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 24/30] 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 7adb60e0e6eee0a091a581d3814f72916246ad5d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 18:29:06 +0300 Subject: [PATCH 25/30] fixed error msg in the tests --- .../java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 90ecba923b..8fa8c387fd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -201,7 +201,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID was too long '" + invalidClientId + "'. " + + .hasMessage("Client ID is too long '" + invalidClientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); @@ -221,7 +221,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { String resultedClientId = validClientId + "_" + serviceId; assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID was too long '" + resultedClientId + "'. " + + .hasMessage("Client ID is too long '" + resultedClientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); From bf6bd064b7e4eb3fb82bf0db820a4616dd0aad2b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 4 Sep 2024 12:01:13 +0300 Subject: [PATCH 26/30] 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, From 1c2d1f8780e5a7d1a14a37e4b02afe1fdc8ce13c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 5 Sep 2024 10:19:05 +0300 Subject: [PATCH 27/30] UI: Improve widget layout settings: Add preserve aspect ratio and resizable options. --- .../core/services/dashboard-utils.service.ts | 7 +- .../add-widget-dialog.component.ts | 2 + .../dashboard-page.component.ts | 2 +- .../widget/widget-config.component.html | 36 ++-- .../widget/widget-config.component.ts | 22 ++- .../home/models/dashboard-component.models.ts | 176 +++++++++++++++--- .../src/app/shared/models/dashboard.models.ts | 2 + ui-ngx/src/app/shared/models/widget.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 6 +- 9 files changed, 206 insertions(+), 49 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 53d1df8219..2196969d01 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -345,10 +345,11 @@ export class DashboardUtilsService { return widgetConfig; } - public prepareWidgetForScadaLayout(widget: Widget): Widget { + public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { const config = widget.config; config.showTitle = false; config.dropShadow = false; + config.preserveAspectRatio = isScada; config.padding = '0'; config.margin = '0'; config.backgroundColor = 'rgba(0,0,0,0)'; @@ -654,7 +655,9 @@ export class DashboardUtilsService { mobileOrder: widget.config.mobileOrder, mobileHeight: widget.config.mobileHeight, mobileHide: widget.config.mobileHide, - desktopHide: widget.config.desktopHide + desktopHide: widget.config.desktopHide, + preserveAspectRatio: widget.config.preserveAspectRatio, + resizable: widget.config.resizable }; if (isUndefined(originalColumns)) { originalColumns = 24; 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 1d2fb7bfcf..7a2b550201 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 @@ -187,6 +187,8 @@ export class AddWidgetDialogComponent extends DialogComponent
-
-
- - {{ 'widget-config.mobile-hide' | translate }} - -
-
- - {{ 'widget-config.desktop-hide' | translate }} - +
+
+
widget-config.resize-options
+
+ + {{ 'widget-config.resizable' | translate }} + +
+
+ + {{ 'widget-config.preserve-aspect-ratio' | translate }} + +
-
+
+
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
+
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+
+ + {{ 'widget-config.desktop-hide' | translate }} + +
widget-config.order
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 f554c4bf3f..643a5da990 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 @@ -256,6 +256,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe }); this.layoutSettings = this.fb.group({ + resizable: [true], + preserveAspectRatio: [false], mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]], mobileHide: [false], @@ -349,16 +351,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe value: 'actions' } ); - if (this.showLayoutConfig) { - this.headerOptions.push( - { - name: this.isDefaultBreakpoint - ? this.translate.instant('widget-config.mobile') - : this.translate.instant('widget-config.list-layout'), - value: 'mobile' - } - ); - } + this.headerOptions.push( + { + name: this.translate.instant('widget-config.layout'), + value: 'layout' + } + ); if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) { this.selectedOption = this.headerOptions[0].value; } @@ -569,6 +567,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe if (layout) { this.layoutSettings.patchValue( { + resizable: isDefined(layout.resizable) ? layout.resizable : true, + preserveAspectRatio: layout.preserveAspectRatio, mobileOrder: layout.mobileOrder, mobileHeight: layout.mobileHeight, mobileHide: layout.mobileHide, @@ -579,6 +579,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } else { this.layoutSettings.patchValue( { + resizable: true, + preserveAspectRatio: false, mobileOrder: null, mobileHeight: null, mobileHide: false, diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index e7fdfa78e3..ed9ca5123f 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -343,6 +343,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private selectedValue = false; private selectedCallback: (selected: boolean) => void = () => {}; + resizableHandles = {} as any; + + resizeEnabled = true; + isFullscreen = false; isReference = false; @@ -387,6 +391,15 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private gridsterItemComponentSubject = new Subject(); private gridsterItemComponentValue: GridsterItemComponentInterface; + private readonly aspectRatio: number; + + private heightValue: number; + private widthValue: number; + + private rowsValue: number; + private colsValue: number; + + get mobileHide(): boolean { return this.widgetLayout ? this.widgetLayout.mobileHide === true : false; } @@ -397,10 +410,96 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { set gridsterItemComponent(item: GridsterItemComponentInterface) { this.gridsterItemComponentValue = item; + + if (this.widgetLayout?.preserveAspectRatio) { + this.applyPreserveAspectRatio(item); + } + this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue); this.gridsterItemComponentSubject.complete(); } + private applyPreserveAspectRatio(item: GridsterItemComponentInterface) { + this.resizableHandles.ne = false; + this.resizableHandles.sw = false; + this.resizableHandles.nw = false; + + const $item = item.$item; + + this.rowsValue = $item.rows; + this.colsValue = $item.cols; + + Object.defineProperty($item, 'rows', { + get: () => this.rowsValue, + set: v => { + if (this.rowsValue !== v) { + if (this.preserveAspectRatio) { + this.colsValue = v * this.aspectRatio; + } + this.rowsValue = v; + } + } + }); + + Object.defineProperty($item, 'cols', { + get: () => this.colsValue, + set: v => { + if (this.colsValue !== v) { + if (this.preserveAspectRatio) { + this.rowsValue = v / this.aspectRatio; + } + this.colsValue = v; + } + } + }); + + const resizable = item.resize; + + this.heightValue = resizable.height; + this.widthValue = resizable.width; + + const setItemHeight = resizable.setItemHeight.bind(resizable); + const setItemWidth = resizable.setItemWidth.bind(resizable); + resizable.setItemHeight = (height) => { + setItemHeight(height); + this.heightValue = height; + if (this.preserveAspectRatio) { + setItemWidth(height * this.aspectRatio); + } + }; + resizable.setItemWidth = (width) => { + setItemWidth(width); + this.widthValue = width; + if (this.preserveAspectRatio) { + setItemHeight(width / this.aspectRatio); + } + }; + + Object.defineProperty(resizable, 'height', { + get: () => this.heightValue, + set: v => { + if (this.heightValue !== v) { + if (this.preserveAspectRatio) { + this.widthValue = v * this.aspectRatio; + } + this.heightValue = v; + } + } + }); + + Object.defineProperty(resizable, 'width', { + get: () => this.widthValue, + set: v => { + if (this.widthValue !== v) { + if (this.preserveAspectRatio) { + this.heightValue = v / this.aspectRatio; + } + this.widthValue = v; + } + } + }); + } + get highlighted() { return this.highlightedValue; } @@ -434,6 +533,13 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { public widgetLayout?: WidgetLayout, private parentDashboard?: IDashboardComponent, private popoverComponent?: TbPopoverComponent) { + + if (isDefined(widgetLayout?.resizable)) { + this.resizeEnabled = widgetLayout.resizable; + } + if (widgetLayout?.preserveAspectRatio) { + this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY; + } if (!widget.id) { widget.id = guid(); } @@ -603,30 +709,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { } } - @enumerable(true) - get cols(): number { - let res; - if (this.widgetLayout) { - res = this.widgetLayout.sizeX; + get preserveAspectRatio(): boolean { + if (!this.dashboard.isMobileSize && this.widgetLayout) { + return this.widgetLayout.preserveAspectRatio; } else { - res = this.widget.sizeX; + return false; } - return Math.floor(res); + } + + @enumerable(true) + get cols(): number { + return Math.floor(this.sizeX); } set cols(cols: number) { if (!this.dashboard.isMobileSize) { - if (this.widgetLayout) { - this.widgetLayout.sizeX = cols; - } else { - this.widget.sizeX = cols; - } + this.sizeX = cols; } } @enumerable(true) get rows(): number { - let res; + let res: number; if (this.dashboard.isMobileSize) { let mobileHeight; if (this.widgetLayout) { @@ -638,26 +742,50 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { if (mobileHeight) { res = mobileHeight; } else { - const sizeY = this.widgetLayout ? this.widgetLayout.sizeY : this.widget.sizeY; + const sizeY = this.sizeY; res = sizeY * 24 / this.dashboard.gridsterOpts.minCols; } } else { - if (this.widgetLayout) { - res = this.widgetLayout.sizeY; - } else { - res = this.widget.sizeY; - } + res = this.sizeY; } return Math.floor(res); } set rows(rows: number) { if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) { - if (this.widgetLayout) { - this.widgetLayout.sizeY = rows; - } else { - this.widget.sizeY = rows; - } + this.sizeY = rows; + } + } + + get sizeX(): number { + if (this.widgetLayout) { + return this.widgetLayout.sizeX; + } else { + return this.widget.sizeX; + } + } + + set sizeX(sizeX: number) { + if (this.widgetLayout) { + this.widgetLayout.sizeX = sizeX; + } else { + this.widget.sizeX = sizeX; + } + } + + get sizeY(): number { + if (this.widgetLayout) { + return this.widgetLayout.sizeY; + } else { + return this.widget.sizeY; + } + } + + set sizeY(sizeY: number) { + if (this.widgetLayout) { + this.widgetLayout.sizeY = sizeY; + } else { + this.widget.sizeY = sizeY; } } diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 2621175030..c4e767b314 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -43,6 +43,8 @@ export interface WidgetLayout { mobileOrder?: number; col?: number; row?: number; + resizable?: boolean; + preserveAspectRatio?: boolean; } export interface WidgetLayouts { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 3b60cd727e..429bb79e35 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -761,6 +761,8 @@ export interface WidgetConfig { displayTimewindow?: boolean; timewindow?: Timewindow; timewindowStyle?: TimewindowStyle; + resizable?: boolean; + preserveAspectRatio?: boolean; desktopHide?: boolean; mobileHide?: boolean; mobileHeight?: 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 4dd7125f72..aaa51ea3dd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6045,7 +6045,11 @@ "color": "Color", "tooltip": "Tooltip", "units-required": "Unit is required.", - "list-layout": "List layout" + "list-layout": "List layout", + "layout": "Layout", + "resize-options": "Resize options", + "resizable": "Resizable", + "preserve-aspect-ratio": "Preserve aspect ratio" }, "widget-type": { "import": "Import widget type", From 7a84dc655dbff28805e54061d14275f375c884f8 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 5 Sep 2024 12:23:43 +0300 Subject: [PATCH 28/30] added catch for DataValidationException --- .../engine/aws/lambda/TbAwsLambdaNode.java | 5 ++++- .../aws/lambda/TbAwsLambdaNodeTest.java | 22 ++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 06dc0310ae..1dbc81bfa1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @@ -65,8 +66,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; - validateFields(config, errorPrefix); try { + validateFields(config, errorPrefix); AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) @@ -75,6 +76,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { .withConnectionTimeout((int) TimeUnit.SECONDS.toMillis(config.getConnectionTimeout())) .withRequestTimeout((int) TimeUnit.SECONDS.toMillis(config.getRequestTimeout()))) .build(); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); } catch (Exception e) { throw new TbNodeException(e); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java index d730b9b837..14ae9bef20 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java @@ -36,6 +36,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; @@ -43,7 +44,6 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -99,7 +99,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidFunctionName_whenInit_thenThrowsException(String funcName) { config.setFunctionName(funcName); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -107,7 +107,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { config.setAccessKey(accessKey); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -115,7 +115,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { config.setSecretKey(secretAccessKey); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -123,19 +123,19 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidRegion_whenInit_thenThrowsException(String region) { config.setRegion(region); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @Test public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { config.setConnectionTimeout(-100); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @Test public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { config.setRequestTimeout(-100); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -318,14 +318,16 @@ public class TbAwsLambdaNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg); } - private void verifyDataValidationExceptionOnInit() { + private void verifyValidationExceptionOnInit() { RuleNode ruleNode = new RuleNode(); ruleNode.setName("test"); when(ctx.getSelf()).thenReturn(ruleNode); String errorPrefix = "'test' node configuration is invalid: "; assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) - .isInstanceOf(DataValidationException.class) - .hasMessageContaining(errorPrefix); + .isInstanceOf(TbNodeException.class) + .hasMessageContaining(errorPrefix) + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } private void init() { From 6f2801db0aab0b901524196cc2040f3ff6b3d955 Mon Sep 17 00:00:00 2001 From: Iryna Matveieva <101514424+irynamatveieva@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:38:52 +0300 Subject: [PATCH 29/30] Fixed incorrect display of device state (#11536) * changed logic to save to db activity value from cache * changed check if partition belongs --- .../state/DefaultDeviceStateService.java | 63 +++++----- .../state/DefaultDeviceStateServiceTest.java | 108 ++++++++++++++++++ 2 files changed, 141 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 1c53920a93..6025e874b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -157,7 +157,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService() { @Override - public void onSuccess(@Nullable DeviceStateData state) { + public void onSuccess(DeviceStateData state) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, device.getId()); - if (addDeviceUsingState(tpi, state)) { - save(deviceId, ACTIVITY_STATE, false); + Set deviceIds = partitionedEntities.get(tpi); + boolean isMyPartition = deviceIds != null; + if (isMyPartition) { + deviceIds.add(state.getDeviceId()); + initializeActivityState(deviceId, state); callback.onSuccess(); } else { - log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}" - , tenantId, deviceId, tpi.getFullTopicName()); + log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}", tenantId, deviceId, tpi.getFullTopicName()); callback.onFailure(new RuntimeException("Device belongs to external partition " + tpi.getFullTopicName() + "!")); } } @@ -400,6 +403,21 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIdSet = partitionedEntities.get(tpi); + if (deviceIdSet != null) { + deviceIdSet.remove(deviceId); + } + } + + private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) { + DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState); + boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive(); + save(deviceId, ACTIVITY_STATE, activityState); + } + @Override protected Map>> onAddedPartitions(Set addedPartitions) { var result = new HashMap>>(); @@ -436,10 +454,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIds = partitionedEntities.get(tpi); + boolean isMyPartition = deviceIds != null; + if (isMyPartition) { + deviceIds.add(state.getDeviceId()); + deviceStates.putIfAbsent(state.getDeviceId(), state); + checkAndUpdateState(state.getDeviceId(), state); + } else { + log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName()); } - checkAndUpdateState(state.getDeviceId(), state); } log.info("[{}] Initialized {} out of {} device states", entry.getKey().getPartition().orElse(0), counter.addAndGet(states.size()), entry.getValue().size()); } @@ -475,18 +499,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIds = partitionedEntities.get(tpi); - if (deviceIds != null) { - deviceIds.add(state.getDeviceId()); - deviceStates.putIfAbsent(state.getDeviceId(), state); - return true; - } else { - log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName()); - return false; - } - } - void checkStates() { try { final long ts = getCurrentTimeMillis(); @@ -619,15 +631,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIdSet = partitionedEntities.get(tpi); - if (deviceIdSet != null) { - deviceIdSet.remove(deviceId); - } - } - @Override protected void cleanupEntityOnPartitionRemoval(DeviceId deviceId) { cleanupEntity(deviceId); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 9833396a0a..296191d61b 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.state; +import com.google.common.util.concurrent.Futures; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,8 +29,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; @@ -41,11 +44,13 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.sql.query.EntityQueryRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; @@ -66,6 +71,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -1070,4 +1076,106 @@ public class DefaultDeviceStateServiceTest { then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId); } + @Test + public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + + TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + + // WHEN + service.onQueueMsg(proto, TbCallback.EMPTY); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any()); + }); + } + + @Test + public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + long currentTime = System.currentTimeMillis(); + DeviceState deviceState = DeviceState.builder() + .active(false) + .inactivityTimeout(service.getDefaultInactivityTimeoutInSec()) + .build(); + DeviceStateData stateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .deviceCreationTime(currentTime - 10000) + .state(deviceState) + .metaData(TbMsgMetaData.EMPTY) + .build(); + service.deviceStates.put(deviceId, stateData); + + // WHEN + service.onDeviceActivity(tenantId, deviceId, currentTime); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(LAST_ACTIVITY_TIME), eq(currentTime), any()); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any()); + }); + } + + @Test + public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + + long currentTime = System.currentTimeMillis(); + DeviceState deviceState = DeviceState.builder() + .active(true) + .lastConnectTime(currentTime - 8000) + .lastActivityTime(currentTime - 4000) + .lastDisconnectTime(0) + .lastInactivityAlarmTime(0) + .inactivityTimeout(3000) + .build(); + DeviceStateData stateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .deviceCreationTime(currentTime - 10000) + .state(deviceState) + .build(); + service.deviceStates.put(deviceId, stateData); + + // WHEN + TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + service.onQueueMsg(proto, TbCallback.EMPTY); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any()); + }); + } + } From cfcf627fe10de244ca69711859c3a14d4792ee8a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 5 Sep 2024 12:48:55 +0300 Subject: [PATCH 30/30] UI: Improve widget resizable options. --- .../core/services/dashboard-utils.service.ts | 1 + .../add-widget-dialog.component.ts | 9 +++++++- .../widget/widget-config.component.ts | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 6 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 2196969d01..5c2363789d 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -349,6 +349,7 @@ export class DashboardUtilsService { const config = widget.config; config.showTitle = false; config.dropShadow = false; + config.resizable = !isScada; config.preserveAspectRatio = isScada; config.padding = '0'; config.margin = '0'; 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 7a2b550201..709159aec3 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 @@ -130,7 +130,14 @@ export class AddWidgetDialogComponent extends DialogComponent { - this.updateWidgetSettingsEnabledState(); - }); - this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => { + merge(this.widgetSettings.get('showTitle').valueChanges, + this.widgetSettings.get('showTitleIcon').valueChanges).subscribe(() => { this.updateWidgetSettingsEnabledState(); }); @@ -263,6 +261,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe mobileHide: [false], desktopHide: [false] }); + + this.layoutSettings.get('resizable').valueChanges.subscribe(() => { + this.updateLayoutEnabledState(); + }); + this.actionsSettings = this.fb.group({ actions: [null, []] }); @@ -589,6 +592,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe {emitEvent: false} ); } + this.updateLayoutEnabledState(); } this.createChangeSubscriptions(); } @@ -622,6 +626,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } } + private updateLayoutEnabledState() { + const resizable: boolean = this.layoutSettings.get('resizable').value; + if (resizable) { + this.layoutSettings.get('preserveAspectRatio').enable({emitEvent: false}); + } else { + this.layoutSettings.get('preserveAspectRatio').disable({emitEvent: false}); + } + } + private updateSchemaForm(settings?: any) { const widgetSettingsFormData: JsonFormComponentData = {}; if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {