Browse Source

Merge remote-tracking branch 'upstream/master' into github_action_yml_check

pull/9362/head
dashevchenko 3 years ago
parent
commit
2ccaa76ecc
  1. 13
      application/src/main/data/json/system/widget_bundles/weather_widgets.json
  2. 29
      application/src/main/data/json/system/widget_types/wind_speed_and_direction.json
  3. 1
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  4. 4
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  5. 5
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  6. 4
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  7. 1
      ui-ngx/package.json
  8. 4
      ui-ngx/src/app/core/utils.ts
  9. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  10. 37
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  11. 25
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  12. 138
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  13. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  14. 17
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  15. 213
      ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html
  16. 291
      ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts
  17. 4
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html
  18. 214
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss
  19. 6
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts
  20. 102
      ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html
  21. 139
      ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts
  22. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  23. 25
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.html
  24. 49
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.scss
  25. 333
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts
  26. 108
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts
  27. 9
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  28. 41
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  29. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  30. 187
      ui-ngx/src/assets/widget/wind-speed-direction/advanced-layout.svg
  31. 167
      ui-ngx/src/assets/widget/wind-speed-direction/default-layout.svg
  32. 153
      ui-ngx/src/assets/widget/wind-speed-direction/simplified-layout.svg
  33. 12
      ui-ngx/src/form.scss
  34. 5
      ui-ngx/yarn.lock

13
application/src/main/data/json/system/widget_bundles/weather_widgets.json

@ -0,0 +1,13 @@
{
"widgetsBundle": {
"alias": "weather_widgets",
"title": "Weather widgets",
"image": null,
"description": null,
"externalId": null,
"name": "Weather widgets"
},
"widgetTypeFqns": [
"wind_speed_and_direction"
]
}

29
application/src/main/data/json/system/widget_types/wind_speed_and_direction.json

@ -0,0 +1,29 @@
{
"fqn": "wind_speed_and_direction",
"name": "Wind speed and direction",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAilBMVEXg4ODf39/g4OAAAADg4ODf39/////g4OD7+/v39/fn5+f09PTx8fEhISHr6+vu7u7k5OSenp7i4uLOzs6qqqqQkJDt7e3IyMjw8PDp6em2trbV1dU8PDwvLy/b29vCwsJXV1eCgoKGhoa6urptbW10dHRKSkqkpKRmZma8vLywsLCSkpJ7e3thYWHl47mIAAAABnRSTlPvIL8Ar7DvmsykAAAHPklEQVR42uzZbY+aQBSGYfuSh+ecmWFgRFBBq2m2u9v0//++grtGq9uUioIa70Txk3o5c8wYR18+fR5Ft97XT19Gn0aeuPFoa8ZXizvI19vq5tejiaNRhLsoekCurAfk2npArq0H5Np6QK6tB+TaekDaV4wBfcVhtweZp4plhsNuEJJlch+Q8Xx8JxDzPL4PCMbpnUAkuweI5oA5fpXbg3zYA/KAdM2ABoawILHfDUEsrIVSYmQlFd4ggRjsGhRCbm7k9tGHcfO2G0NMmnH6bD2sUN9oJDA4JFHAK9TCBEINjhNBgtpgCKLOpGk6f/eZRmM8ZHhIMA0kjxEHYcRjBoyHFxLbsrRuiW1ELkyOF5MqgJ3CaB0vD/GBXiEJknha3x9EqDDGfnnaNP9T6xEbEPsxSog4hlcRweUhMvNewUgSUTs7dOgH4yymTnDQDBIfQvwGEgPoA8KgCugsRlCL/cwUOdE6TyTch0iQBpKoTvuAwEYK5JGBBsEuSyZo2U5uuYPQ6vvWYi8QqAImImzYHw47w39HKrmDQEPHrdU9FRicIBHEZgeRqNlacRxzIIgFc5wWPbih0BAwAjF1Q0Fyj9OjEryKs9aMkC4QQbS8AogVmaFbSWE5PKQsBd2KoynVDAwhWEbdJD6awkzBYSF2RmroIAlPv9ZZUQRRDgkhLEFVnJxdVa5pTchwEFHZcIhO2bVzE/jZYBCCBmdo4lYuhQE5EESnOEP2ya2D+w4wMQNBYNG98MOlKFwBiGAQiCRE98pqUaCGlKizOgQEtOhctqhCc3EWAD0HgFhzljF/sagrXrCJsfQOYSxnGPMJ/sgb9A0RnmfMDxPpGWKUnce8Kj942p4hPNeYH8deIUl8ljE/ThLb84ycdcx3kegR0vl3UOZS/CVOfX8QSI5uBfwtsewPIrhkZG+QWNnmUy/CKWdMr+aq/noLK+fcKuwU6WqClvUFmbY5nVTuKXtxK7xVNqyWEKPSE4QtzhGFqwC7cAGbJounl9YQQQ+QtsP43f0AUG0h1mLSFgKyJ8gsxj+zi0WoNRW2tYdIkl/TsJcLV7kfYR9ybcPuvbQ6TC1WlZucAjGx9AKhsM2MVBZh4YoTICLsB4IWrV0K4MmlJ0DAXiDUhG0gawAvJ0FsYi8FGRvkJVAatK1wbvJ97ZqvrkV6yrCbcV1+bsi8RPEMSXMA9K04aeWcWxRA6lb/CaG3gKaXgBTf8DNTfUYdjUGriqx8u1o02WDbQoxpIJfYWlFmnouynKNOtMngQuXa9L4iZ4dIunzV+WuJumXatMSFmqd12aUgyH63d647TkJRGPW62Bc8UEAKVVutjcYZ5/1fz3aMoU47sQjDZdKV9ld/NCvsDw6cfQ43ay0KA3A7oDT0f0TKpyotNgtntWgq+EkR14PIer3uPexEG4jy5pzypEiSgG/2RBMYNHZjOlf2LsQWT2esdZZpjbUQ/1+ViY1+y6C0ZGvtuhB0QneIYL429h+wHXhON/oX0VBzCbuft5+3t59XsFqxuVvcORehVjGdO0Rgt+a2YLOFHWwLcrv4D7qI9G+yMxYr8i3rLdh2t3UuQoWBRMqgrUS2a1g529WllxEd/3HQYnlG5CdQ7BZ3RsMUwg5Jqpzly/FdoIMfvvkGwHLnIhLTwURcHum9evd+XvMjaMUp2cdPX+mMuzCYiNTOCcvDfHN3JCTjzuouDg962zCFWV3E7OwDue5oWg0pgvQS8wl0PuDWY8wb4iAMLCJ9xrzBfWARpNKTmHemchhaJMS9xny8DjpQJ+s15sTA8CJu+vXT10OTaG8kNoYIYN/e9RdzVGAckTqJPn2mJ8bsxBbkpregj9kbj5j20cjsAlAGGE1EiEXSTOiCRibEgo66osf2Hko3PDJGXtGDSJorXfEPEcKoIlR5TXfiZTn68j1Fu6qooeOvQ4TQLacIiTO8iLiCKKggyj1J1XGt7j3qAqJ7hhDxNJhJbEikf079Uv7/tUQq1DlgVmcxITOzIUQsgdQ1w6OEpm0gdZT2iGAx92gkuBHCQKUVpy4gmYRgRNrUuQfaI812L5IGBUI9UGnhFlVgnkrm2bGhiAmt8AqXRiqkqd6XVjyEiAqSKWWdUlvNMXFF1UIl0b/MRaEcsLQsQTLHo0AcxScDcdQva+RNUOMYzYTECLWIDCEiFmUJSOr7r578Sq1UPKDIT1YkUCv6QC2NTCnTPVPojXc8kCjymIiQqKSowvjzI/9KkTW7OR2LCC5ixBUOTF4EUEr/bVOJ+F5EfGr7a7UgaXY8y8VIHJvQjmftEBxxlhElohwzL5FHuYpcRXrgw3LtnDA/kc3N+kfBCfMTKSJ0ozxkfiIfio1xyvxEKDfFkhNmJ6K54gvnIbMTocht+f05ZMSXq3MhmZ/IWa4iV5G5cBWZGleRqXEVmRpXkalxFZkaz0jkxfN4QXD04lXFMyB+u3/7dDz7YyLVXuP1yzfR3Hnx6uXrXzaOLHMeGQCLAAAAAElFTkSuQmCC",
"description": "Displays the latest values of the wind speed and direction.",
"descriptor": {
"type": "latest",
"sizeX": 3,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-wind-speed-direction-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-wind-speed-direction-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 2,\n singleEntity: true,\n previewWidth: '270px',\n previewHeight: '270px',\n embedTitlePanel: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-wind-speed-direction-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-wind-speed-direction-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"windDirection\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 360;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 360) {\\n\\tvalue = 360;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"centerValue\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 30) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m/s\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"centerValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"centerValueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"ticksColor\":\"rgba(0, 0, 0, 0.12)\",\"directionalNamesElseDegrees\":true,\"majorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"majorTicksColor\":\"rgba(158, 158, 158, 1)\",\"minorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"minorTicksColor\":\"rgba(0, 0, 0, 0.12)\",\"arrowColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind speed and direction\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"air\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
},
"externalId": null,
"tags": [
"wind",
"weather",
"compass",
"degrees"
]
}

1
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -532,6 +532,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
this.deleteSystemWidgetBundle("html_widgets");
this.deleteSystemWidgetBundle("tables");
this.deleteSystemWidgetBundle("count_widgets");
this.deleteSystemWidgetBundle("weather_widgets");
installScripts.loadSystemWidgets();
}

4
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -61,6 +61,7 @@ import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import java.net.URISyntaxException;
import java.util.Arrays;
@ -90,6 +91,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected NotificationTargetService notificationTargetService;
@Autowired
protected NotificationRequestService notificationRequestService;
@Autowired
protected SqlPartitioningRepository partitioningRepository;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@ -100,6 +103,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID);
notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1);
}
protected NotificationTarget createNotificationTarget(UserId... usersIds) {

5
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -286,18 +286,19 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void whenTenantIsDeleted_thenDeleteNotificationRequests() throws Exception {
createDifferentTenant();
TenantId tenantId = differentTenantId;
NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId());
int notificationsCount = 20;
for (int i = 0; i < notificationsCount; i++) {
NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB);
awaitNotificationRequest(request.getId());
}
List<NotificationRequest> requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(differentTenantId, EntityType.USER, new PageLink(100)).getData();
List<NotificationRequest> requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData();
assertThat(requests).size().isEqualTo(notificationsCount);
deleteDifferentTenant();
assertThat(notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(differentTenantId, EntityType.USER, new PageLink(1)).getTotalElements())
assertThat(notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(1)).getTotalElements())
.isZero();
}

4
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -456,9 +456,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
loginSysAdmin();
notifications = await().atMost(30, TimeUnit.SECONDS)
.until(() -> getMyNotifications(true, 10).stream()
.filter(notification -> notification.getType() == NotificationType.RATE_LIMITS)
.collect(Collectors.toList()), list -> list.size() == 1);
.until(() -> getMyNotifications(true, 10), list -> list.size() == 1);
assertThat(notifications).allSatisfy(notification -> {
assertThat(notification.getSubject()).isEqualTo("Rate limits exceeded for tenant " + TEST_TENANT_NAME);
});

1
ui-ngx/package.json

@ -44,6 +44,7 @@
"@ngrx/store-devtools": "^15.4.0",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@svgdotjs/svg.js": "^3.2.0",
"@tinymce/tinymce-angular": "^7.0.0",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",

4
ui-ngx/src/app/core/utils.ts

@ -768,7 +768,7 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
if (datasource) {
if (datasource.dataKeys) {
datasource.dataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
if (dataKey?.label === label) {
i++;
label = name + ' ' + i;
matches = true;
@ -777,7 +777,7 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
}
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
if (dataKey?.label === label) {
i++;
label = name + ' ' + i;
matches = true;

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -58,6 +58,9 @@ import {
import {
BatteryLevelBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/battery-level-basic-config.component';
import {
WindSpeedDirectionBasicConfigComponent
} from '@home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component';
@NgModule({
declarations: [
@ -75,7 +78,8 @@ import {
DataKeysPanelComponent,
AlarmCountBasicConfigComponent,
EntityCountBasicConfigComponent,
BatteryLevelBasicConfigComponent
BatteryLevelBasicConfigComponent,
WindSpeedDirectionBasicConfigComponent
],
imports: [
CommonModule,
@ -97,7 +101,8 @@ import {
DataKeysPanelComponent,
AlarmCountBasicConfigComponent,
EntityCountBasicConfigComponent,
BatteryLevelBasicConfigComponent
BatteryLevelBasicConfigComponent,
WindSpeedDirectionBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -113,5 +118,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-aggregated-value-card-basic-config': AggregatedValueCardBasicConfigComponent,
'tb-alarm-count-basic-config': AlarmCountBasicConfigComponent,
'tb-entity-count-basic-config': EntityCountBasicConfigComponent,
'tb-battery-level-basic-config': BatteryLevelBasicConfigComponent
'tb-battery-level-basic-config': BatteryLevelBasicConfigComponent,
'tb-wind-speed-direction-basic-config': WindSpeedDirectionBasicConfigComponent
};

37
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row">
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row" [class]="{'tb-single-row': singleRow}">
<mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>
@ -23,8 +23,8 @@
</mat-select>
</mat-form-field>
<mat-form-field class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
<mat-chip-grid #chipList [formControl]="keysFormControl">
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type"
(removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels">
@ -67,8 +67,8 @@
#keyInput
[formControl]="keyFormControl"
matAutocompleteOrigin
[fxHide]="!!modelValue.type"
[readonly]="!!modelValue.type"
[fxHide]="!!modelValue?.type"
[readonly]="!!modelValue?.type"
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
(focusin)="onKeyInputFocus()"
@ -139,7 +139,7 @@
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<mat-form-field *ngIf="!hideDataKeyLabel" class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div *ngIf="!hideDataKeyColor" class="tb-color-field">
@ -147,25 +147,28 @@
formControlName="color">
</tb-color-input>
</div>
<div *ngIf="!hideUnits" class="tb-units-field">
<div *ngIf="!hideUnits && !hideDataKeyUnits && (!singleRow || displayUnitsOrDigits)" class="tb-units-field">
<tb-unit-input *ngIf="displayUnitsOrDigits"
formControlName="units">
</tb-unit-input>
</div>
<div *ngIf="!hideDecimals" class="tb-decimals-field">
<div *ngIf="!hideDecimals && !hideDataKeyDecimals && (!singleRow || displayUnitsOrDigits)" class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div *ngIf="singleRow" matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell-buttons">
<button fxHide.lt-lg
type="button"
mat-icon-button
(click)="editKey(true)"
[matTooltip]="keySettingsTitle"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<div *ngIf="!singleRow" class="tb-form-table-row-cell-buttons">
<div fxHide.lt-lg class="tb-settings-button">
<button *ngIf="modelValue"
type="button"
mat-icon-button
(click)="editKey(true)"
[matTooltip]="keySettingsTitle"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
</div>
<button type="button"
mat-icon-button
(click)="keyRemoved.emit()"

25
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -88,4 +88,29 @@
display: block;
}
}
.tb-settings-button {
width: 40px;
min-width: 40px;
}
&.tb-single-row {
padding: 0;
.tb-units-field, .tb-decimals-field {
min-width: 0;
width: auto;
}
.tb-units-field {
tb-unit-input {
width: auto;
max-width: 140px;
}
}
.tb-decimals-field {
.mat-mdc-form-field {
width: auto;
max-width: 140px;
}
}
}
}

138
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -35,7 +35,8 @@ import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors
ValidationErrors,
Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
@ -47,7 +48,6 @@ import {
Widget,
widgetType
} from '@shared/models/widget.models';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
@ -65,6 +65,9 @@ import {
import { deepClone } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => {
const dataKey: DataKey = control.value;
@ -104,6 +107,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
required = false;
@Input()
datasourceType: DatasourceType;
@ -113,9 +120,52 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Input()
deviceId: string;
@Input()
@coerceBoolean()
hasAdditionalLatestDataKeys = false;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@Input()
@coerceBoolean()
hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideUnits = false;
@Input()
@coerceBoolean()
hideDecimals = false;
@Input()
@coerceBoolean()
singleRow = true;
@Input()
dataKeyType: DataKeyType;
@Input()
keySettingsTitle: string;
@Input()
removeKeyTitle: string;
@Output()
keyRemoved = new EventEmitter();
keysFormControl: UntypedFormControl;
keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup;
@ -126,33 +176,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
keySearchText = '';
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
private latestKeySearchTextResult: Array<DataKey> = null;
private keyFetchObservable$: Observable<Array<DataKey>> = null;
get dataKeyType(): DataKeyType {
return this.dataKeysPanelComponent.dataKeyType;
}
get alarmKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.alarmKeys;
}
get functionTypeKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.functionTypeKeys;
}
get hideDataKeyColor(): boolean {
return this.dataKeysPanelComponent.hideDataKeyColor;
}
get hideUnits(): boolean {
return this.dataKeysPanelComponent.hideUnits;
}
get hideDecimals(): boolean {
return this.dataKeysPanelComponent.hideDecimals;
}
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
@ -161,10 +190,6 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get hasAdditionalLatestDataKeys(): boolean {
return this.dataKeysPanelComponent.hasAdditionalLatestDataKeys;
}
get widget(): Widget {
return this.widgetConfigComponent.widget;
}
@ -198,19 +223,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
get displayUnitsOrDigits() {
return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type);
}
get keySettingsTitle(): string {
return this.dataKeysPanelComponent.keySettingsTitle;
}
get removeKeyTitle(): string {
return this.dataKeysPanelComponent.removeKeyTitle;
}
get dragEnabled(): boolean {
return this.dataKeysPanelComponent.dragEnabled;
return this.modelValue?.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue?.type);
}
get isLatestDataKeys(): boolean {
@ -224,12 +237,27 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
private cd: ChangeDetectorRef,
public translate: TranslateService,
public truncate: TruncatePipe,
private dataKeysPanelComponent: DataKeysPanelComponent,
private utils: UtilsService,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({
name,
type: DataKeyType.alarm
});
}
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
this.keyFormControl = this.fb.control('');
this.keysFormControl = this.fb.control([], this.required ? [Validators.required] : []);
this.keyRowFormGroup = this.fb.group({
label: [null, []],
color: [null, []],
@ -300,14 +328,16 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keysFormControl.disable({emitEvent: false});
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keysFormControl.enable({emitEvent: false});
this.keyRowFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey): void {
this.modelValue = value || {} as DataKey;
this.modelValue = (value?.name && value?.type) ? value : null;
this.keyRowFormGroup.patchValue(
{
label: value?.label,
@ -321,6 +351,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
latest: (value as any)?.latest
}, {emitEvent: false});
}
this.keysFormControl.patchValue(this.modelValue ? [this.modelValue] : [], {emitEvent: false});
this.cd.markForCheck();
}
@ -368,10 +399,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks,
hideDataKeyLabel: false,
hideDataKeyLabel: this.hideDataKeyLabel,
hideDataKeyColor: this.hideDataKeyColor,
hideDataKeyUnits: !this.displayUnitsOrDigits,
hideDataKeyDecimals: !this.displayUnitsOrDigits
hideDataKeyUnits: this.hideDataKeyUnits || !this.displayUnitsOrDigits,
hideDataKeyDecimals: this.hideDataKeyDecimals || !this.displayUnitsOrDigits
}
}).afterClosed().subscribe((updatedDataKey) => {
if (updatedDataKey) {
@ -387,7 +418,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
removeKey() {
this.modelValue = {} as DataKey;
this.modelValue = null;
this.updateModel();
this.clearKeyChip();
}
@ -409,7 +440,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
onKeyInputFocus() {
if (!this.modelValue.type) {
if (!this.modelValue?.type) {
this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
}
}
@ -479,8 +510,11 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
private updateModel() {
const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.keysFormControl.patchValue(this.modelValue ? [this.modelValue] : [], {emitEvent: false});
if (this.modelValue !== null) {
const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
}
this.propagateChange(this.modelValue);
}

9
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -36,9 +36,18 @@
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"
required
[datasourceType]="datasourceType"
[deviceId]="deviceId"
[entityAliasId]="entityAliasId"
[hasAdditionalLatestDataKeys]="hasAdditionalLatestDataKeys"
[hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals"
[hideUnits]="hideUnits"
[dataKeyType]="dataKeyType"
[singleRow]="false"
[keySettingsTitle]="keySettingsTitle"
[removeKeyTitle]="removeKeyTitle"
(keyRemoved)="removeKey($index)">
</tb-data-key-row>
<div class="tb-form-table-row-cell-buttons">

17
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -41,7 +41,6 @@ import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared
import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@ -113,8 +112,6 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
hideSourceSelection = false;
dataKeyType: DataKeyType;
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
keysListFormGroup: UntypedFormGroup;
@ -165,20 +162,6 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
this.keysListFormGroup.valueChanges.subscribe(
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value)
);
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({
name,
type: DataKeyType.alarm
});
}
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
this.updateParams();
}

213
ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html

@ -0,0 +1,213 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="windSpeedDirectionWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeys
hideDatasourceLabel
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.wind-speed-direction.values</div>
<div class="tb-form-row">
<div class="fixed-title-width tb-required" translate>widgets.wind-speed-direction.wind-direction</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-data-key-row fxFlex
formControlName="windDirectionKey"
required
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
hideDataKeyLabel
hideDataKeyColor
hideDataKeyDecimals
hideDataKeyUnits>
</tb-data-key-row>
<tb-font-settings *ngIf="!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value"
formControlName="centerValueFont"
[autoScale]="true"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetConfigForm.get('centerValueColor').value?.color }"
[previewText]="centerValuePreviewFn">
</tb-font-settings>
<tb-color-settings *ngIf="!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value"
formControlName="centerValueColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>widgets.wind-speed-direction.center-value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-data-key-row fxFlex
formControlName="centerValueKey"
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
hideDataKeyLabel
hideDataKeyColor>
</tb-data-key-row>
<tb-font-settings *ngIf="windSpeedDirectionWidgetConfigForm.get('centerValueKey').value"
formControlName="centerValueFont"
[autoScale]="true"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetConfigForm.get('centerValueColor').value?.color }"
[previewText]="centerValuePreviewFn">
</tb-font-settings>
<tb-color-settings *ngIf="windSpeedDirectionWidgetConfigForm.get('centerValueKey').value"
formControlName="centerValueColor">
</tb-color-settings>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
cols="3"
colsLtMd="2"
label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of windSpeedDirectionLayouts"
[value]="layout"
[image]="windSpeedDirectionLayoutImageMap.get(layout)">
{{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="windSpeedDirectionWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.wind-speed-direction.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="windSpeedDirectionWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.wind-speed-direction.ticks</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="ticksColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.labels-type' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="directionalNamesElseDegrees">
<mat-option [value]="true">
{{ 'widgets.wind-speed-direction.directional-names' | translate }}
</mat-option>
<mat-option [value]="false">
{{ 'widgets.wind-speed-direction.degrees' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.major-ticks' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings [fxShow]="majorTicksFontEnabled"
formControlName="majorTicksFont"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetConfigForm.get('majorTicksColor').value }"
[autoScale]="true"
previewText="N E S W">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="majorTicksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.minor-ticks' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings [fxShow]="minorTicksFontEnabled"
formControlName="minorTicksFont"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetConfigForm.get('minorTicksColor').value }"
[autoScale]="true"
previewText="NE SE SW NW">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="minorTicksColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.wind-speed-direction.arrow</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="arrowColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

291
ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts

@ -0,0 +1,291 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Injector } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
DataKey,
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import {
cssSizeToStrSize, getDataKey,
getDataKeyByLabel,
resolveCssSize,
updateDataKeyByLabel
} from '@shared/models/widget-settings.models';
import {
centerValueLabel,
windDirectionLabel,
windSpeedDirectionDefaultSettings,
WindSpeedDirectionLayout,
windSpeedDirectionLayoutImages,
windSpeedDirectionLayouts,
windSpeedDirectionLayoutTranslations,
WindSpeedDirectionWidgetSettings
} from '@home/components/widget/lib/weather/wind-speed-direction-widget.models';
@Component({
selector: 'tb-wind-speed-direction-basic-config',
templateUrl: './wind-speed-direction-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class WindSpeedDirectionBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.windSpeedDirectionWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get displayTimewindowConfig(): boolean {
const datasources = this.windSpeedDirectionWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.windSpeedDirectionWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
windSpeedDirectionLayouts = windSpeedDirectionLayouts;
windSpeedDirectionLayoutTranslationMap = windSpeedDirectionLayoutTranslations;
windSpeedDirectionLayoutImageMap = windSpeedDirectionLayoutImages;
windSpeedDirectionWidgetConfigForm: UntypedFormGroup;
centerValuePreviewFn = this._centerValuePreviewFn.bind(this);
get majorTicksFontEnabled(): boolean {
const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetConfigForm.get('layout').value;
return [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(layout);
}
get minorTicksFontEnabled(): boolean {
const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetConfigForm.get('layout').value;
return layout === WindSpeedDirectionLayout.advanced;
}
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.windSpeedDirectionWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'winddirection', label: windDirectionLabel, type: DataKeyType.timeseries },
{ name: 'windspeed', label: centerValueLabel, type: DataKeyType.timeseries,
units: 'm/s', decimals: 1 }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: WindSpeedDirectionWidgetSettings = {...windSpeedDirectionDefaultSettings, ...(configData.config.settings || {})};
const iconSize = resolveCssSize(configData.config.iconSize);
let windDirectionDataKey = getDataKeyByLabel(configData.config.datasources, windDirectionLabel);
if (!windDirectionDataKey) {
windDirectionDataKey = getDataKey(configData.config.datasources);
}
this.windSpeedDirectionWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
windDirectionKey: [windDirectionDataKey, [Validators.required]],
centerValueKey: [getDataKeyByLabel(configData.config.datasources, centerValueLabel), []],
centerValueFont: [settings.centerValueFont, []],
centerValueColor: [settings.centerValueColor, []],
layout: [settings.layout, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
ticksColor: [settings.ticksColor, []],
directionalNamesElseDegrees: [settings.directionalNamesElseDegrees, []],
majorTicksFont: [settings.majorTicksFont, []],
majorTicksColor: [settings.majorTicksColor, []],
minorTicksFont: [settings.minorTicksFont, []],
minorTicksColor: [settings.minorTicksColor, []],
arrowColor: [settings.arrowColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
updateDataKeyByLabel(this.widgetConfig.config.datasources, config.windDirectionKey, windDirectionLabel);
updateDataKeyByLabel(this.widgetConfig.config.datasources, config.centerValueKey, centerValueLabel);
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.centerValueFont = config.centerValueFont;
this.widgetConfig.config.settings.centerValueColor = config.centerValueColor;
this.widgetConfig.config.settings.ticksColor = config.ticksColor;
this.widgetConfig.config.settings.directionalNamesElseDegrees = config.directionalNamesElseDegrees;
this.widgetConfig.config.settings.majorTicksFont = config.majorTicksFont;
this.widgetConfig.config.settings.majorTicksColor = config.majorTicksColor;
this.widgetConfig.config.settings.minorTicksFont = config.minorTicksFont;
this.widgetConfig.config.settings.minorTicksColor = config.minorTicksColor;
this.widgetConfig.config.settings.arrowColor = config.arrowColor;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['layout', 'showTitle', 'showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetConfigForm.get('layout').value;
const showTitle: boolean = this.windSpeedDirectionWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.windSpeedDirectionWidgetConfigForm.get('showIcon').value;
const majorTicksFontEnabled = [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(layout);
const minorTicksFontEnabled = layout === WindSpeedDirectionLayout.advanced;
if (showTitle) {
this.windSpeedDirectionWidgetConfigForm.get('title').enable();
this.windSpeedDirectionWidgetConfigForm.get('titleFont').enable();
this.windSpeedDirectionWidgetConfigForm.get('titleColor').enable();
this.windSpeedDirectionWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.windSpeedDirectionWidgetConfigForm.get('iconSize').enable();
this.windSpeedDirectionWidgetConfigForm.get('iconSizeUnit').enable();
this.windSpeedDirectionWidgetConfigForm.get('icon').enable();
this.windSpeedDirectionWidgetConfigForm.get('iconColor').enable();
} else {
this.windSpeedDirectionWidgetConfigForm.get('iconSize').disable();
this.windSpeedDirectionWidgetConfigForm.get('iconSizeUnit').disable();
this.windSpeedDirectionWidgetConfigForm.get('icon').disable();
this.windSpeedDirectionWidgetConfigForm.get('iconColor').disable();
}
} else {
this.windSpeedDirectionWidgetConfigForm.get('title').disable();
this.windSpeedDirectionWidgetConfigForm.get('titleFont').disable();
this.windSpeedDirectionWidgetConfigForm.get('titleColor').disable();
this.windSpeedDirectionWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.windSpeedDirectionWidgetConfigForm.get('iconSize').disable();
this.windSpeedDirectionWidgetConfigForm.get('iconSizeUnit').disable();
this.windSpeedDirectionWidgetConfigForm.get('icon').disable();
this.windSpeedDirectionWidgetConfigForm.get('iconColor').disable();
}
if (majorTicksFontEnabled) {
this.windSpeedDirectionWidgetConfigForm.get('majorTicksFont').enable();
} else {
this.windSpeedDirectionWidgetConfigForm.get('majorTicksFont').disable();
}
if (minorTicksFontEnabled) {
this.windSpeedDirectionWidgetConfigForm.get('minorTicksFont').enable();
} else {
this.windSpeedDirectionWidgetConfigForm.get('minorTicksFont').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _centerValuePreviewFn(): string {
const centerValueDataKey: DataKey = this.windSpeedDirectionWidgetConfigForm.get('centerValueKey').value;
if (centerValueDataKey) {
let units: string = this.widgetConfig.config.units;
let decimals: number = this.widgetConfig.config.decimals;
if (isDefinedAndNotNull(centerValueDataKey?.decimals)) {
decimals = centerValueDataKey.decimals;
}
if (centerValueDataKey?.units) {
units = centerValueDataKey.units;
}
return formatValue(25, decimals, units, true);
} else {
return '225°';
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html

@ -17,9 +17,7 @@
-->
<div class="tb-battery-level-panel" [style]="backgroundStyle" [class.tb-battery-level-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-battery-level-overlay" [style]="overlayStyle"></div>
<div class="tb-battery-level-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #batteryLevelContent class="tb-battery-level-content" [class]="layoutClass">
<div #batteryLevelBox class="tb-battery-level-box">
<div #batteryLevelRectangle class="tb-battery-level-rectangle" [class]="layoutClass" [class.solid]="solid" [class.divided]="!solid">

214
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss

@ -13,145 +13,143 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-battery-level-panel {
width: 100%;
height: 100%;
position: relative;
.tb-battery-level-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
&.tb-battery-level-pointer {
cursor: pointer;
}
> div:not(.tb-battery-level-overlay) {
z-index: 1;
}
.tb-battery-level-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-battery-level-content {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
&.tb-battery-level-pointer {
cursor: pointer;
}
> div:not(.tb-battery-level-overlay) {
z-index: 1;
justify-content: center;
&.vertical {
flex-direction: row;
gap: 16px;
.tb-battery-level-value-box {
align-items: center;
.tb-battery-level-value {
padding: 8px 12px;
}
}
}
.tb-battery-level-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
&.horizontal {
flex-direction: column-reverse;
gap: 8px;
align-items: center;
.tb-battery-level-value-box {
.tb-battery-level-value {
padding: 4px 6px;
}
}
}
.tb-battery-level-content {
min-height: 0;
flex: 1;
.tb-battery-level-box {
display: flex;
justify-content: center;
&.vertical {
flex-direction: row;
gap: 16px;
.tb-battery-level-value-box {
align-items: center;
.tb-battery-level-value {
padding: 8px 12px;
}
align-items: center;
.tb-battery-level-rectangle {
width: 100%;
height: 100%;
position: relative;
.tb-battery-level-shape {
position: absolute;
inset: 0;
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
}
}
&.horizontal {
flex-direction: column-reverse;
gap: 8px;
align-items: center;
.tb-battery-level-value-box {
.tb-battery-level-value {
padding: 4px 6px;
}
.tb-battery-level-container {
position: absolute;
display: flex;
gap: 3%;
}
}
.tb-battery-level-box {
display: flex;
align-items: center;
.tb-battery-level-rectangle {
.tb-battery-level-indicator-box {
width: 100%;
height: 100%;
position: relative;
&.solid {
background-repeat: no-repeat;
transition: background 0.2s ease-out;
}
&.divided {
transition: opacity 0.2s ease-out;
}
}
&.vertical {
.tb-battery-level-shape {
position: absolute;
inset: 0;
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
mask-image: url(/assets/widget/battery-level/battery-shape-vertical.svg);
}
.tb-battery-level-container {
position: absolute;
display: flex;
gap: 3%;
flex-direction: column-reverse;
}
.tb-battery-level-indicator-box {
width: 100%;
height: 100%;
&.solid {
background-repeat: no-repeat;
transition: background 0.2s ease-out;
}
&.divided {
transition: opacity 0.2s ease-out;
&.solid {
.tb-battery-level-container {
inset: 8.85% 6.25% 3.54% 6.25%;
}
}
&.vertical {
.tb-battery-level-shape {
mask-image: url(/assets/widget/battery-level/battery-shape-vertical.svg);
}
&.divided {
.tb-battery-level-container {
flex-direction: column-reverse;
inset: 9.73% 7.81% 4.42% 7.81%;
}
}
.tb-battery-level-indicator-box {
&.solid {
.tb-battery-level-container {
inset: 8.85% 6.25% 3.54% 6.25%;
}
border-radius: 10.7% / 6%;
background-position: 0 101%;
}
&.divided {
.tb-battery-level-container {
inset: 9.73% 7.81% 4.42% 7.81%;
}
}
.tb-battery-level-indicator-box {
&.solid {
border-radius: 10.7% / 6%;
background-position: 0 101%;
}
&.divided {
border-radius: 7.14% / 17.8%;
}
border-radius: 7.14% / 17.8%;
}
}
&.horizontal {
.tb-battery-level-shape {
mask-image: url(/assets/widget/battery-level/battery-shape-horizontal.svg);
}
}
&.horizontal {
.tb-battery-level-shape {
mask-image: url(/assets/widget/battery-level/battery-shape-horizontal.svg);
}
.tb-battery-level-container {
inset: 6.25% 8.85% 6.25% 3.54%;
flex-direction: row;
}
&.solid {
.tb-battery-level-container {
inset: 6.25% 8.85% 6.25% 3.54%;
flex-direction: row;
}
}
&.divided {
.tb-battery-level-container {
inset: 7.81% 9.73% 7.81% 4.42%;
}
}
.tb-battery-level-indicator-box {
&.solid {
.tb-battery-level-container {
inset: 6.25% 8.85% 6.25% 3.54%;
}
border-radius: 6% / 10.7%;
background-position: -1% 0%;
}
&.divided {
.tb-battery-level-container {
inset: 7.81% 9.73% 7.81% 4.42%;
}
}
.tb-battery-level-indicator-box {
&.solid {
border-radius: 6% / 10.7%;
background-position: -1% 0%;
}
&.divided {
border-radius: 17.8% / 7.14%;
}
border-radius: 17.8% / 7.14%;
}
}
}
}
.tb-battery-level-value-box {
display: flex;
.tb-battery-level-value {
white-space: nowrap;
}
}
.tb-battery-level-value-box {
display: flex;
.tb-battery-level-value {
white-space: nowrap;
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts

@ -24,7 +24,8 @@ import {
OnInit,
Renderer2,
TemplateRef,
ViewChild
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
@ -74,7 +75,8 @@ const horizontalBatteryDimensions = {
@Component({
selector: 'tb-battery-level-widget',
templateUrl: './battery-level-widget.component.html',
styleUrls: ['./battery-level-widget.component.scss']
styleUrls: ['./battery-level-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterViewInit {

102
ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html

@ -0,0 +1,102 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="windSpeedDirectionWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.wind-speed-direction.wind-speed-direction-card-style</div>
<tb-image-cards-select rowHeight="1:1"
cols="3"
colsLtMd="2"
label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of windSpeedDirectionLayouts"
[value]="layout"
[image]="windSpeedDirectionLayoutImageMap.get(layout)">
{{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row space-between">
<div>{{ (hasCenterValue ? 'widgets.wind-speed-direction.center-value' : 'widgets.wind-speed-direction.wind-direction') | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="centerValueFont"
[autoScale]="true"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetSettingsForm.get('centerValueColor').value?.color }"
[previewText]="centerValuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="centerValueColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.ticks-color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="ticksColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.ticks-labels-type' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="directionalNamesElseDegrees">
<mat-option [value]="true">
{{ 'widgets.wind-speed-direction.directional-names' | translate }}
</mat-option>
<mat-option [value]="false">
{{ 'widgets.wind-speed-direction.degrees' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.major-ticks' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings [fxShow]="majorTicksFontEnabled"
formControlName="majorTicksFont"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetSettingsForm.get('majorTicksColor').value }"
[autoScale]="true"
previewText="N E S W">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="majorTicksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.minor-ticks' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings [fxShow]="minorTicksFontEnabled"
formControlName="minorTicksFont"
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetSettingsForm.get('minorTicksColor').value }"
[autoScale]="true"
previewText="NE SE SW NW">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="minorTicksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.wind-speed-direction.arrow-color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="arrowColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</ng-container>

139
ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts

@ -0,0 +1,139 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Injector } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import {
centerValueLabel,
windSpeedDirectionDefaultSettings,
WindSpeedDirectionLayout,
windSpeedDirectionLayoutImages,
windSpeedDirectionLayouts,
windSpeedDirectionLayoutTranslations
} from '@home/components/widget/lib/weather/wind-speed-direction-widget.models';
import { getDataKeyByLabel } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-wind-speed-direction-widget-settings',
templateUrl: './wind-speed-direction-widget-settings.component.html',
styleUrls: []
})
export class WindSpeedDirectionWidgetSettingsComponent extends WidgetSettingsComponent {
get hasCenterValue(): boolean {
return !!getDataKeyByLabel(this.widgetConfig.config.datasources, centerValueLabel);
}
get majorTicksFontEnabled(): boolean {
const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetSettingsForm.get('layout').value;
return [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(layout);
}
get minorTicksFontEnabled(): boolean {
const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetSettingsForm.get('layout').value;
return layout === WindSpeedDirectionLayout.advanced;
}
windSpeedDirectionLayouts = windSpeedDirectionLayouts;
windSpeedDirectionLayoutTranslationMap = windSpeedDirectionLayoutTranslations;
windSpeedDirectionLayoutImageMap = windSpeedDirectionLayoutImages;
windSpeedDirectionWidgetSettingsForm: UntypedFormGroup;
centerValuePreviewFn = this._centerValuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.windSpeedDirectionWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...windSpeedDirectionDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.windSpeedDirectionWidgetSettingsForm = this.fb.group({
layout: [settings.layout, []],
centerValueFont: [settings.centerValueFont, []],
centerValueColor: [settings.centerValueColor, []],
ticksColor: [settings.ticksColor, []],
directionalNamesElseDegrees: [settings.directionalNamesElseDegrees, []],
majorTicksFont: [settings.majorTicksFont, []],
majorTicksColor: [settings.majorTicksColor, []],
minorTicksFont: [settings.minorTicksFont, []],
minorTicksColor: [settings.minorTicksColor, []],
arrowColor: [settings.arrowColor, []],
background: [settings.background, []]
});
}
protected validatorTriggers(): string[] {
return ['layout'];
}
protected updateValidators(emitEvent: boolean) {
const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetSettingsForm.get('layout').value;
const majorTicksFontEnabled = [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(layout);
const minorTicksFontEnabled = layout === WindSpeedDirectionLayout.advanced;
if (majorTicksFontEnabled) {
this.windSpeedDirectionWidgetSettingsForm.get('majorTicksFont').enable();
} else {
this.windSpeedDirectionWidgetSettingsForm.get('majorTicksFont').disable();
}
if (minorTicksFontEnabled) {
this.windSpeedDirectionWidgetSettingsForm.get('minorTicksFont').enable();
} else {
this.windSpeedDirectionWidgetSettingsForm.get('minorTicksFont').disable();
}
}
private _centerValuePreviewFn(): string {
const centerValueDataKey = getDataKeyByLabel(this.widgetConfig.config.datasources, centerValueLabel);
if (centerValueDataKey) {
let units: string = this.widgetConfig.config.units;
let decimals: number = this.widgetConfig.config.decimals;
if (isDefinedAndNotNull(centerValueDataKey?.decimals)) {
decimals = centerValueDataKey.decimals;
}
if (centerValueDataKey?.units) {
units = centerValueDataKey.units;
}
return formatValue(25, decimals, units, true);
} else {
return '225°';
}
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -288,6 +288,9 @@ import {
import {
BatteryLevelWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/battery-level-widget-settings.component';
import {
WindSpeedDirectionWidgetSettingsComponent
} from '@home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component';
@NgModule({
declarations: [
@ -394,7 +397,8 @@ import {
AggregatedValueCardWidgetSettingsComponent,
AlarmCountWidgetSettingsComponent,
EntityCountWidgetSettingsComponent,
BatteryLevelWidgetSettingsComponent
BatteryLevelWidgetSettingsComponent,
WindSpeedDirectionWidgetSettingsComponent
],
imports: [
CommonModule,
@ -506,7 +510,8 @@ import {
AggregatedValueCardWidgetSettingsComponent,
AlarmCountWidgetSettingsComponent,
EntityCountWidgetSettingsComponent,
BatteryLevelWidgetSettingsComponent
BatteryLevelWidgetSettingsComponent,
WindSpeedDirectionWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -583,5 +588,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-aggregated-value-card-widget-settings': AggregatedValueCardWidgetSettingsComponent,
'tb-alarm-count-widget-settings': AlarmCountWidgetSettingsComponent,
'tb-entity-count-widget-settings': EntityCountWidgetSettingsComponent,
'tb-battery-level-widget-settings': BatteryLevelWidgetSettingsComponent
'tb-battery-level-widget-settings': BatteryLevelWidgetSettingsComponent,
'tb-wind-speed-direction-widget-settings': WindSpeedDirectionWidgetSettingsComponent
};

25
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.html

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-wind-speed-direction-panel" [style]="backgroundStyle" [class.tb-wind-speed-direction-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-wind-speed-direction-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-wind-speed-direction-content">
<div #windSpeedDirectionShape class="tb-wind-speed-direction-shape">
</div>
</div>
</div>

49
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.scss

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-wind-speed-direction-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
&.tb-wind-speed-direction-pointer {
cursor: pointer;
}
> div:not(.tb-wind-speed-direction-overlay) {
z-index: 1;
}
.tb-wind-speed-direction-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-wind-speed-direction-content {
flex: 1;
min-width: 0;
min-height: 0;
.tb-wind-speed-direction-shape {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}

333
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts

@ -0,0 +1,333 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild, ViewEncapsulation
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import {
centerValueLabel,
windDirectionLabel,
windSpeedDirectionDefaultSettings,
WindSpeedDirectionLayout,
WindSpeedDirectionWidgetSettings
} from '@home/components/widget/lib/weather/wind-speed-direction-widget.models';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle,
Font, getDataKey,
getDataKeyByLabel,
getSingleTsValueByDataKey,
overlayStyle
} from '@shared/models/widget-settings.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer';
import { Path, Svg, SVG, Text } from '@svgdotjs/svg.js';
import { DataKey } from '@shared/models/widget.models';
const shapeSize = 180;
const cx = shapeSize / 2;
const cy = shapeSize / 2;
const ticksDiameter = 140;
const ticksTextMap: {[angle: number]: string} = {
0: 'N',
45: 'NE',
90: 'E',
135: 'SE',
180: 'S',
225: 'SW',
270: 'W',
315: 'NW'
};
@Component({
selector: 'tb-wind-speed-direction-widget',
templateUrl: './wind-speed-direction-widget.component.html',
styleUrls: ['./wind-speed-direction-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('windSpeedDirectionShape', {static: false})
windSpeedDirectionShape: ElementRef<HTMLElement>;
settings: WindSpeedDirectionWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
layout: WindSpeedDirectionLayout;
centerValueColor: ColorProcessor;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
shapeResize$: ResizeObserver;
hasCardClickAction = false;
private decimals = 0;
private units = '';
private drawSvgShapePending = false;
private svgShape: Svg;
private arrow: Path;
private centerValueTextNode: Text;
private windDirectionDataKey: DataKey;
private centerValueDataKey: DataKey;
private windDirection = 0;
private centerValueText = 'N/A';
constructor(private widgetComponent: WidgetComponent,
private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.windSpeedDirectionWidget = this;
this.settings = {...windSpeedDirectionDefaultSettings, ...this.ctx.settings};
this.windDirectionDataKey = getDataKeyByLabel(this.ctx.datasources, windDirectionLabel);
if (!this.windDirectionDataKey) {
this.windDirectionDataKey = getDataKey(this.ctx.datasources);
}
this.centerValueDataKey = getDataKeyByLabel(this.ctx.datasources, centerValueLabel);
if (this.centerValueDataKey) {
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
if (isDefinedAndNotNull(this.centerValueDataKey.decimals)) {
this.decimals = this.centerValueDataKey.decimals;
}
if (this.centerValueDataKey.units) {
this.units = this.centerValueDataKey.units;
}
}
this.layout = this.settings.layout;
this.centerValueColor = ColorProcessor.fromSettings(this.settings.centerValueColor);
this.backgroundStyle = backgroundStyle(this.settings.background);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
}
ngAfterViewInit() {
if (this.drawSvgShapePending) {
this.drawSvg();
}
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
if (this.windSpeedDirectionShape) {
this.drawSvg();
} else {
this.drawSvgShapePending = true;
}
this.cd.detectChanges();
}
public onDataUpdated() {
let centerValue = 0;
this.windDirection = 0;
this.centerValueText = 'N/A';
const windDirectionTsValue = getSingleTsValueByDataKey(this.ctx.data, this.windDirectionDataKey);
if (windDirectionTsValue && isDefinedAndNotNull(windDirectionTsValue[1]) && isNumeric(windDirectionTsValue[1])) {
this.windDirection = windDirectionTsValue[1];
}
if (this.centerValueDataKey) {
const centerValueTsValue = getSingleTsValueByDataKey(this.ctx.data, this.centerValueDataKey);
if (centerValueTsValue && isDefinedAndNotNull(centerValueTsValue[1]) && isNumeric(centerValueTsValue[1])) {
centerValue = centerValueTsValue[1];
this.centerValueText = formatValue(centerValue, this.decimals, '', true);
}
}
this.centerValueColor.update(centerValue);
this.renderValues();
}
public cardClick($event: Event) {
this.ctx.actionsApi.cardClick($event);
}
private drawSvg() {
this.svgShape = SVG().addTo(this.windSpeedDirectionShape.nativeElement).size(shapeSize, shapeSize);
this.renderer.setStyle(this.svgShape.node, 'overflow', 'visible');
this.renderer.setStyle(this.svgShape.node, 'user-select', 'none');
// Draw ticks
const ticksYStart = (shapeSize - ticksDiameter) / 2;
for (let i = 0; i < 360; i += 3) {
if (i !== 0) {
let color: string;
let width: number;
let height: number;
if (i % 90 === 0) {
// Major ticks
color = this.settings.majorTicksColor;
width = 2;
height = 8;
} else if (i % 45 === 0) {
// Minor ticks
color = this.settings.minorTicksColor;
width = 2;
height = 8;
} else {
color = this.settings.ticksColor;
width = 1.2;
height = 3;
}
this.svgShape.line(cx, ticksYStart, cx, ticksYStart + height).attr({
'stroke-width': width,
stroke: color
}).rotate(i, cx, cy);
}
}
// Draw pointer
this.svgShape.path('m 89.152,20.470002 c 0.3917,-0.626669 1.3043,-0.626669 1.696,0 l 3.1958,5.1132 ' +
'c 0.4162,0.66605 -0.0626,1.53 -0.848,1.53 h -6.3916 c -0.7854,0 -1.2642,-0.86395 -0.848,-1.53 z')
.fill(this.settings.majorTicksColor);
let x: number;
let y: number;
let degree: number;
const drawMajorTicksText = [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(this.settings.layout);
const drawMinorTicksText = this.settings.layout === WindSpeedDirectionLayout.advanced;
if (drawMajorTicksText) {
// Draw major ticks text
for (let i = 0; i < 4; i += 1) {
degree = i * 90;
if (i % 2 === 0) {
x = cx;
y = i === 0 ? 10 : shapeSize - 10;
} else {
y = cy;
x = i === 3 ? 10 : shapeSize - 10;
}
this.drawTickText(degree, this.settings.majorTicksFont, this.settings.majorTicksColor, x, y);
}
}
if (drawMinorTicksText) {
// Draw minor ticks text
for (let i = 0; i < 4; i += 1) {
degree = 45 + (i * 90);
if (i < 2) {
x = shapeSize - 30;
y = i === 0 ? 30 : shapeSize - 30;
} else {
x = 30;
y = i === 3 ? 30 : shapeSize - 30;
}
this.drawTickText(degree, this.settings.minorTicksFont, this.settings.minorTicksColor, x, y);
}
}
// Draw arrow
this.arrow = this.svgShape.path('m 89.263587,23.438382 c 0.388942,-0.392146 1.022181,-0.388549 1.414649,0 ' +
'l 6.389758,6.389 c 0.392414,0.388462 0.394911,1.022828 0.0059,1.415 -0.388987,0.392109 -1.022226,0.383311 -1.41408,-0.006 ' +
'l -4.6762,-4.676 v 28.417 h -2 v -28.417 l -4.637642,4.676 ' +
'c -0.388878,0.392069 -1.022053,0.394895 -1.414202,0.006 -0.392082,-0.388967 -0.394683,-1.022852 -0.0057,-1.415 ' +
'z M 88.983614,154.85438 h -2.217 v 2 h 6.434 v -2 h -2.217 v -29.939 h -2 z').fill(this.settings.arrowColor);
// Draw value
this.centerValueTextNode = this.svgShape.text('').font({
family: this.settings.centerValueFont.family,
weight: this.settings.centerValueFont.weight,
style: this.settings.centerValueFont.style
}).attr({x: '50%', y: '50%', 'text-anchor': 'middle'});
if (!this.units) {
this.centerValueTextNode.attr({'dominant-baseline': 'middle'});
}
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.windSpeedDirectionShape.nativeElement);
this.onResize();
this.renderValues();
}
private drawTickText(degree: number, font: Font, color: string, x: number, y: number) {
const tickText = this.settings.directionalNamesElseDegrees ? ticksTextMap[degree] : degree + '';
this.svgShape.text(tickText).font({
family: font.family,
weight: font.weight,
style: font.style,
size: this.settings.directionalNamesElseDegrees ? '14px' : '10px'
}).fill(color).center(x, y);
}
private renderValues() {
if (this.svgShape) {
this.arrow.timeline().finish();
this.arrow.animate(800).transform({rotate: this.windDirection});
this.renderCenterValueText();
}
}
private renderCenterValueText() {
const text = this.centerValueDataKey ? this.centerValueText : formatValue(this.windDirection, 0, '') + '°';
this.centerValueTextNode.text(add => {
add.tspan(text).font({size: '24px'});
if (this.units) {
add.tspan(this.units).newLine().font({size: '14px'});
}
}).fill(this.centerValueColor.color);
}
private onResize() {
const shapeWidth = this.windSpeedDirectionShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.windSpeedDirectionShape.nativeElement.getBoundingClientRect().height;
const size = Math.min(shapeWidth, shapeHeight);
const scale = size / shapeSize;
this.renderer.setStyle(this.svgShape.node, 'transform', `scale(${scale})`);
}
}

108
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BatteryLevelLayout } from '@home/components/widget/lib/indicator/battery-level-widget.models';
import {
BackgroundSettings,
BackgroundType,
ColorSettings,
constantColor,
Font
} from '@shared/models/widget-settings.models';
export enum WindSpeedDirectionLayout {
default = 'default',
advanced = 'advanced',
simplified = 'simplified'
}
export const windSpeedDirectionLayouts = Object.keys(WindSpeedDirectionLayout) as WindSpeedDirectionLayout[];
export const windSpeedDirectionLayoutTranslations = new Map<WindSpeedDirectionLayout, string>(
[
[WindSpeedDirectionLayout.default, 'widgets.wind-speed-direction.layout-default'],
[WindSpeedDirectionLayout.advanced, 'widgets.wind-speed-direction.layout-advanced'],
[WindSpeedDirectionLayout.simplified, 'widgets.wind-speed-direction.layout-simplified']
]
);
export const windSpeedDirectionLayoutImages = new Map<WindSpeedDirectionLayout, string>(
[
[WindSpeedDirectionLayout.default, 'assets/widget/wind-speed-direction/default-layout.svg'],
[WindSpeedDirectionLayout.advanced, 'assets/widget/wind-speed-direction/advanced-layout.svg'],
[WindSpeedDirectionLayout.simplified, 'assets/widget/wind-speed-direction/simplified-layout.svg']
]
);
export interface WindSpeedDirectionWidgetSettings {
layout: WindSpeedDirectionLayout;
centerValueFont: Font;
centerValueColor: ColorSettings;
ticksColor: string;
arrowColor: string;
directionalNamesElseDegrees: boolean;
majorTicksColor: string;
majorTicksFont: Font;
minorTicksColor: string;
minorTicksFont: Font;
background: BackgroundSettings;
}
export const windSpeedDirectionDefaultSettings: WindSpeedDirectionWidgetSettings = {
layout: WindSpeedDirectionLayout.default,
centerValueFont: {
family: 'Roboto',
size: 24,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '32px'
},
centerValueColor: constantColor('rgba(0, 0, 0, 0.87)'),
ticksColor: 'rgba(0, 0, 0, 0.12)',
arrowColor: 'rgba(0, 0, 0, 0.87)',
directionalNamesElseDegrees: true,
majorTicksColor: 'rgba(158, 158, 158, 1)',
majorTicksFont: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
minorTicksColor: 'rgba(0, 0, 0, 0.12)',
minorTicksFont: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};
export const windDirectionLabel = 'windDirection';
export const centerValueLabel = 'centerValue';

9
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -58,6 +58,9 @@ import {
} from '@home/components/widget/lib/cards/aggregated-value-card-widget.component';
import { CountWidgetComponent } from '@home/components/widget/lib/count/count-widget.component';
import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicator/battery-level-widget.component';
import {
WindSpeedDirectionWidgetComponent
} from '@home/components/widget/lib/weather/wind-speed-direction-widget.component';
@NgModule({
declarations:
@ -92,7 +95,8 @@ import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicat
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
BatteryLevelWidgetComponent
BatteryLevelWidgetComponent,
WindSpeedDirectionWidgetComponent
],
imports: [
CommonModule,
@ -131,7 +135,8 @@ import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicat
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
BatteryLevelWidgetComponent
BatteryLevelWidgetComponent,
WindSpeedDirectionWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

41
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -426,6 +426,37 @@ export const getDataKey = (datasources?: Datasource[]): DataKey => {
return null;
};
export const getDataKeyByLabel = (datasources: Datasource[], label: string): DataKey => {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
return dataKeys.find(k => k.label === label);
}
}
return null;
};
export const updateDataKeyByLabel = (datasources: Datasource[], dataKey: DataKey, label: string): void => {
if (datasources && datasources.length) {
let dataKeys = datasources[0].dataKeys;
if (!dataKeys) {
dataKeys = [];
datasources[0].dataKeys = dataKeys;
}
const existingIndex = dataKeys.findIndex(k => k.label === label || k === dataKey);
if (dataKey) {
dataKey.label = label;
if (existingIndex > -1) {
dataKeys[existingIndex] = dataKey;
} else {
dataKeys.push(dataKey);
}
} else if (existingIndex > -1) {
dataKeys.splice(existingIndex, 1);
}
}
};
export const getAlarmFilterConfig = (datasources?: Datasource[]): AlarmFilterConfig => {
if (datasources && datasources.length) {
const config = datasources[0].alarmFilterConfig;
@ -467,6 +498,16 @@ export const getSingleTsValue = (data: Array<DatasourceData>): [number, any] =>
return null;
};
export const getSingleTsValueByDataKey = (data: Array<DatasourceData>, dataKey: DataKey): [number, any] => {
if (data.length) {
const dsData = data.find(d => d.dataKey === dataKey);
if (dsData?.data?.length) {
return dsData.data[0];
}
}
return null;
};
export const getLatestSingleTsValue = (data: Array<DatasourceData>): [number, any] => {
if (data.length) {
const dsData = data[0];

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

@ -6144,6 +6144,27 @@
"table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode"
},
"wind-speed-direction": {
"layout": "Layout",
"layout-default": "Default",
"layout-advanced": "Advanced",
"layout-simplified": "Simplified",
"values": "Values",
"wind-direction": "Wind direction",
"center-value": "Center value",
"icon": "Icon",
"arrow": "Arrow",
"ticks": "Ticks",
"labels-type": "Labels type",
"directional-names": "Directional names",
"degrees": "Degrees",
"major-ticks": "Major ticks",
"minor-ticks": "Minor ticks",
"wind-speed-direction-card-style": "Wind speed and direction card style",
"ticks-color": "Ticks color",
"ticks-labels-type": "Ticks labels type",
"arrow-color": "Arrow color"
},
"value-source": {
"value-source": "Value source",
"predefined-value": "Predefined value",

187
ui-ngx/src/assets/widget/wind-speed-direction/advanced-layout.svg

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="134" height="133" fill="none" version="1.1" viewBox="0 0 134 133" xmlns="http://www.w3.org/2000/svg">
<rect x="8.8335" y="4.5" width="117" height="117" rx="1.7594" fill="#fff" filter="url(#filter0_d_3060_521241)"/>
<defs>
<filter id="filter0_d_3060_521241" x=".8335" y=".5" width="133" height="133" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3060_521241"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow_3060_521241" result="shape"/>
</filter>
</defs>
<g>
<g transform="matrix(.44244 0 0 .44244 -18.017 33.104)" fill="#000" stroke="#000" stroke-opacity=".11765" stroke-width="2">
<line x1="242.4" x2="236.74" y1="16.763" y2="22.42"/>
<line x1="242.4" x2="236.74" y1="115.76" y2="110.1"/>
<line x1="143.4" x2="149.06" y1="115.76" y2="110.1"/>
<line x1="143.4" x2="149.06" y1="16.763" y2="22.42"/>
</g>
<g transform="matrix(.44244 0 0 .44244 27.511 22.601)" fill="#000" stroke="#000" stroke-opacity=".12" stroke-width="1.2">
<line transform="rotate(3,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(6,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(9,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(12,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(15,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(18,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(21,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(24,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(27,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(30,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(33,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(36,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(39,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(42,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(48,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(51,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(54,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(57,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(60,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(63,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(66,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(69,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(72,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(75,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(78,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(81,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(84,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(87,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(93,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(96,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(99,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(102,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(105,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(108,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(111,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(114,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(117,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(120,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(123,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(126,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(129,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(132,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(138,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(141,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(144,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(147,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(150,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(153,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(156,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(159,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(162,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(165,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(168,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(171,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(174,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(177,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(183 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(186 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(189 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(192 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(195 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(198 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(201 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(204 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(207 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(210 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(213 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(216 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(219 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(222 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(228 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(231 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(234 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(237 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(240 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(243 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(246 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(249 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(252 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(255 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(258 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-99,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-96,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-93,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-87,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-84,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-81,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-78,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-75,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-72,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-69,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-66,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-63,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-60,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-57,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-54,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-51,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-48,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-42,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-39,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-36,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-33,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-30,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-27,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-24,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-21,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-18,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-15,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-12,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-9,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-6,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-3,90,90)" x1="90" x2="90" y1="20" y2="23"/>
</g>
<g fill="#9e9e9e" stroke="#9e9e9e" stroke-width=".88487">
<line x1="98.301" x2="94.762" y1="62.42" y2="62.42"/>
<line x1="67.331" x2="67.331" y1="93.391" y2="89.851"/>
<line x1="36.36" x2="39.9" y1="62.42" y2="62.42"/>
</g>
<path d="m66.955 31.658c0.1733-0.27726 0.57707-0.27726 0.75037 0l1.4139 2.2623c0.18414 0.29468-0.0277 0.67693-0.37519 0.67693h-2.8279c-0.34749 0-0.55933-0.38224-0.37519-0.67693z" fill="#9d9d9d" stroke-width=".44244"/>
<path d="m89.681 43.241c0.24352 0.0203 0.42239 0.23594 0.40232 0.47946l-0.3482 3.9826c-0.02004 0.24348-0.23435 0.42473-0.4779 0.40442-0.24352-0.02032-0.42063-0.23745-0.40012-0.48098l0.25494-2.9147-9.6313 8.0816-0.56879-0.67785 9.6313-8.0816-2.9037-0.242c-0.24348-0.0203-0.4245-0.2341-0.40422-0.4776 0.02031-0.24351 0.23442-0.42466 0.47796-0.40435zm-44.62 37.279-0.6305-0.7514-0.67785 0.56878 1.8298 2.1807 0.67785-0.56878-0.6305-0.7514 10.147-8.5144-0.56878-0.67785z" fill="#000" stroke-width=".44244"/>
<g transform="matrix(.44244 0 0 .44244 27.511 22.601)" fill="#000" aria-label="8.1m/s">
<path d="m85.406 85.348q0 2.3789-1.6055 3.6328-1.5938 1.2539-3.9727 1.2539t-3.9961-1.2539q-1.6055-1.2656-1.6055-3.6328 0-1.418 0.73828-2.4961 0.75-1.0781 2.0156-1.6523-1.1016-0.5625-1.7461-1.5469-0.63281-0.98438-0.63281-2.2266 0-2.2617 1.4648-3.4922 1.4766-1.2305 3.75-1.2305 2.2734 0 3.7383 1.2305 1.4766 1.2305 1.4766 3.4922 0 1.2422-0.64453 2.2266-0.64453 0.98438-1.7461 1.5469 1.2656 0.58594 2.0156 1.6641 0.75 1.0664 0.75 2.4844zm-3.2227-7.8164q0-1.125-0.64453-1.8281-0.63281-0.71484-1.7227-0.71484-1.0781 0-1.7227 0.69141-0.64453 0.67969-0.64453 1.8516 0 1.1484 0.63281 1.8516 0.64453 0.70312 1.7461 0.70312 1.0898 0 1.7227-0.70312 0.63281-0.70312 0.63281-1.8516zm0.375 7.6641q0-1.2656-0.76172-2.0391-0.75-0.78516-1.9922-0.78516-1.2539 0-1.9922 0.78516-0.73828 0.77344-0.73828 2.0391 0 1.2891 0.73828 2.0273 0.73828 0.72656 2.0156 0.72656t2.0039-0.72656q0.72656-0.73828 0.72656-2.0273z"/>
<path d="m88.242 88.57q0-0.66797 0.43359-1.125 0.43359-0.45703 1.2188-0.45703t1.2188 0.45703q0.44531 0.45703 0.44531 1.125 0 0.66797-0.44531 1.125-0.43359 0.44531-1.2188 0.44531t-1.2188-0.44531q-0.43359-0.45703-0.43359-1.125z"/>
<path d="m102.35 72.879v17.121h-2.8477v-13.711l-4.1836 1.4297v-2.4023l6.668-2.4375z"/>
<path d="m81.448 102.02q-1.0049 0-1.4219 0.82032v5.3594h-1.6543v-7.3965h1.5586l0.04785 0.77246q0.78613-0.90918 2.1396-0.90918 1.4697 0 2.0234 1.1279 0.81348-1.1279 2.2764-1.1279 1.1279 0 1.7773 0.6289 0.64942 0.62891 0.65625 2.0781v4.8262h-1.6611v-4.7988q0-0.82031-0.3623-1.1006-0.35547-0.28028-0.9707-0.28028-0.56738 0-0.92969 0.30762-0.35547 0.30762-0.49219 0.79981v5.0723h-1.6611v-4.7852q0-0.77929-0.35547-1.0869-0.35547-0.30762-0.9707-0.30762z"/>
<path d="m94.949 98.247-3.917 10.808h-1.3125l3.9238-10.808z"/>
<path d="m100.25 106.19q0-0.35547-0.29395-0.61524-0.28711-0.2666-1.2578-0.47168-1.2031-0.25293-1.9482-0.75195-0.73828-0.50586-0.73828-1.4424 0-0.90918 0.76562-1.5723 0.76562-0.66992 2.085-0.66992 1.3877 0 2.1738 0.67676 0.79297 0.67675 0.79297 1.6748h-1.6611q0-0.43066-0.32813-0.76562-0.32812-0.3418-0.97754-0.3418-0.62891 0-0.92969 0.28027-0.30078 0.28028-0.30078 0.64942 0 0.35546 0.30078 0.57421 0.30078 0.21875 1.2168 0.417 1.2852 0.28027 2.0029 0.77929 0.72461 0.49219 0.72461 1.4834 0 0.98438-0.81348 1.6133-0.81347 0.62891-2.1738 0.62891-1.5244 0-2.3242-0.7793-0.7998-0.77929-0.7998-1.6953h1.6133q0.02734 0.68359 0.49902 0.95703 0.47852 0.27344 1.0254 0.27344 0.65625 0 0.99805-0.25293 0.34864-0.25293 0.34864-0.64941z"/>
</g>
<g transform="translate(-78.085 -.091466)">
<g fill="#9d9d9d">
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="N">
<path d="m93.938 5.0469v9.9531h-1.7295l-4.4434-7.0615v7.0615h-1.7227v-9.9531h1.7227l4.457 7.082v-7.082z" fill="#9d9d9d"/>
</g>
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="E">
<path d="m167.02 95v-9.9531h6.46v1.3877h-4.7373v2.7549h4.0879v1.3604h-4.0879v3.0693h4.7852v1.3809z" fill="#9d9d9d"/>
</g>
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="S">
<path d="m91.979 172.43q0-0.58789-0.39648-0.9502-0.38965-0.3623-1.8115-0.7998-1.415-0.44434-2.3037-1.1279-0.88184-0.69043-0.88184-1.8867 0-1.2031 0.9707-1.9756 0.97754-0.77929 2.5566-0.77929 1.6885 0 2.6387 0.90234 0.9502 0.89551 0.9502 2.1396h-1.7227q0-0.73145-0.45801-1.1963-0.45801-0.47168-1.4287-0.47168-0.90918 0-1.3467 0.39648-0.43066 0.38965-0.43066 0.97754 0 0.56055 0.51953 0.92969 0.51953 0.36231 1.5654 0.66992 1.6885 0.49219 2.4951 1.2305 0.81348 0.73828 0.81348 1.9277 0 1.2578-0.96387 1.9892-0.95703 0.73145-2.5771 0.73145-0.95703 0-1.8525-0.34863-0.88867-0.34864-1.4697-1.0459-0.57422-0.69727-0.57422-1.75h1.7295q0 0.94336 0.62207 1.3604 0.62891 0.41699 1.5449 0.41699 0.90234 0 1.3535-0.36914 0.45801-0.36914 0.45801-0.9707z" fill="#9d9d9d"/>
</g>
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="W">
<path d="m15.565 85.047-2.29 9.9531h-1.6611l-1.8936-7.2598-1.9346 7.2598h-1.6543l-2.2969-9.9531h1.7158l1.5449 7.458 1.9004-7.458h1.4492l1.8799 7.4854 1.5244-7.4854z" fill="#9d9d9d"/>
</g>
</g>
<g fill="#e1e1e1">
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="NE">
<path d="m149.94 25.047v9.9531h-1.7295l-4.4434-7.0615v7.0615h-1.7227v-9.9531h1.7227l4.457 7.082v-7.082z"/>
<path d="m151.99 35v-9.9531h6.46v1.3877h-4.7373v2.7549h4.0879v1.3604h-4.0879v3.0693h4.7852v1.3809z"/>
</g>
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="SE">
<path d="m147.99 152.43q0-0.58789-0.39648-0.9502-0.38965-0.3623-1.8115-0.7998-1.415-0.44434-2.3037-1.1279-0.88183-0.69043-0.88183-1.8867 0-1.2031 0.9707-1.9756 0.97754-0.77929 2.5566-0.77929 1.6885 0 2.6387 0.90234 0.9502 0.89551 0.9502 2.1396h-1.7227q0-0.73145-0.45801-1.1963-0.45801-0.47168-1.4287-0.47168-0.90918 0-1.3467 0.39648-0.43066 0.38965-0.43066 0.97754 0 0.56055 0.51953 0.92969 0.51953 0.36231 1.5654 0.66992 1.6885 0.49219 2.4951 1.2305 0.81347 0.73828 0.81347 1.9277 0 1.2578-0.96387 1.9892-0.95703 0.73145-2.5771 0.73145-0.95703 0-1.8525-0.34863-0.88867-0.34864-1.4697-1.0459-0.57422-0.69727-0.57422-1.75h1.7295q0 0.94336 0.62207 1.3604 0.62891 0.41699 1.5449 0.41699 0.90234 0 1.3535-0.36914 0.45801-0.36914 0.45801-0.9707z"/>
<path d="m151.25 155v-9.9531h6.46v1.3877h-4.7373v2.7549h4.0879v1.3604h-4.0879v3.0693h4.7852v1.3809z"/>
</g>
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="SW">
<path d="m25.486 152.43q0-0.58789-0.39648-0.9502-0.38965-0.3623-1.8115-0.7998-1.415-0.44434-2.3037-1.1279-0.88184-0.69043-0.88184-1.8867 0-1.2031 0.9707-1.9756 0.97754-0.77929 2.5566-0.77929 1.6885 0 2.6387 0.90234 0.9502 0.89551 0.9502 2.1396h-1.7227q0-0.73145-0.45801-1.1963-0.45801-0.47168-1.4287-0.47168-0.90918 0-1.3467 0.39648-0.43066 0.38965-0.43066 0.97754 0 0.56055 0.51953 0.92969 0.51953 0.36231 1.5654 0.66992 1.6885 0.49219 2.4951 1.2305 0.81348 0.73828 0.81348 1.9277 0 1.2578-0.96387 1.9892-0.95703 0.73145-2.5771 0.73145-0.95703 0-1.8525-0.34863-0.88867-0.34864-1.4697-1.0459-0.57422-0.69727-0.57422-1.75h1.7295q0 0.94336 0.62207 1.3604 0.62891 0.41699 1.5449 0.41699 0.90234 0 1.3535-0.36914 0.45801-0.36914 0.45801-0.9707z"/>
<path d="m39.793 145.05-2.29 9.9531h-1.6611l-1.8936-7.2598-1.9346 7.2598h-1.6543l-2.2969-9.9531h1.7158l1.5449 7.458 1.9004-7.458h1.4492l1.8799 7.4854 1.5244-7.4854z"/>
</g>
<g transform="matrix(.44244 0 0 .44244 105.6 22.693)" aria-label="NW">
<path d="m27.441 25.047v9.9531h-1.7295l-4.4434-7.0615v7.0615h-1.7227v-9.9531h1.7227l4.457 7.082v-7.082z"/>
<path d="m40.539 25.047-2.29 9.9531h-1.6611l-1.8936-7.2598-1.9346 7.2598h-1.6543l-2.2969-9.9531h1.7158l1.5449 7.458 1.9004-7.458h1.4492l1.8799 7.4854 1.5244-7.4854z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

167
ui-ngx/src/assets/widget/wind-speed-direction/default-layout.svg

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="134" height="133" fill="none" version="1.1" viewBox="0 0 134 133" xmlns="http://www.w3.org/2000/svg">
<rect x="8.8335" y="4.5" width="117" height="117" rx="1.7594" fill="#fff" filter="url(#filter0_d_3060_521241)"/>
<defs>
<filter id="filter0_d_3060_521241" x=".8335" y=".5" width="133" height="133" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3060_521241"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow_3060_521241" result="shape"/>
</filter>
</defs>
<g transform="matrix(.44244 0 0 .44244 -18.017 33.104)">
<g fill="#000" stroke="#000" stroke-opacity=".11765" stroke-width="2">
<line x1="242.4" x2="236.74" y1="16.763" y2="22.42"/>
<line x1="242.4" x2="236.74" y1="115.76" y2="110.1"/>
<line x1="143.4" x2="149.06" y1="115.76" y2="110.1"/>
<line x1="143.4" x2="149.06" y1="16.763" y2="22.42"/>
</g>
<g transform="translate(102.9 -23.739)" fill="#000" stroke="#000" stroke-opacity=".12" stroke-width="1.2">
<line transform="rotate(3,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(6,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(9,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(12,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(15,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(18,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(21,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(24,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(27,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(30,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(33,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(36,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(39,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(42,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(48,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(51,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(54,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(57,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(60,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(63,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(66,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(69,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(72,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(75,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(78,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(81,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(84,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(87,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(93,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(96,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(99,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(102,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(105,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(108,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(111,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(114,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(117,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(120,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(123,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(126,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(129,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(132,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(138,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(141,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(144,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(147,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(150,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(153,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(156,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(159,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(162,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(165,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(168,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(171,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(174,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(177,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(183 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(186 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(189 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(192 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(195 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(198 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(201 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(204 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(207 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(210 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(213 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(216 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(219 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(222 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(228 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(231 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(234 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(237 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(240 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(243 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(246 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(249 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(252 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(255 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(258 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-99,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-96,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-93,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-87,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-84,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-81,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-78,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-75,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-72,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-69,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-66,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-63,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-60,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-57,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-54,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-51,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-48,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-42,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-39,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-36,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-33,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-30,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-27,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-24,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-21,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-18,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-15,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-12,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-9,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-6,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-3,90,90)" x1="90" x2="90" y1="20" y2="23"/>
</g>
<g fill="#9e9e9e" stroke="#9e9e9e" stroke-width="2">
<line x1="262.9" x2="254.9" y1="66.261" y2="66.261"/>
<line x1="192.9" x2="192.9" y1="136.26" y2="128.26"/>
<line x1="122.9" x2="130.9" y1="66.261" y2="66.261"/>
</g>
<g fill="#9d9d9d">
<path d="m192.05-3.2693c0.3917-0.62667 1.3043-0.62667 1.696 0l3.1958 5.1132c0.4162 0.66605-0.0626 1.53-0.848 1.53h-6.3916c-0.7854 0-1.2642-0.86395-0.848-1.53z"/>
<g transform="translate(102.9 -23.739)" aria-label="N">
<path d="m93.938 5.0469v9.9531h-1.7295l-4.4434-7.0615v7.0615h-1.7227v-9.9531h1.7227l4.457 7.082v-7.082z" fill="#9d9d9d"/>
</g>
<g transform="translate(102.9 -23.739)" aria-label="E">
<path d="m167.02 95v-9.9531h6.46v1.3877h-4.7373v2.7549h4.0879v1.3604h-4.0879v3.0693h4.7852v1.3809z" fill="#9d9d9d"/>
</g>
<g transform="translate(102.9 -23.739)" aria-label="S">
<path d="m91.979 172.43q0-0.58789-0.39648-0.9502-0.38965-0.3623-1.8115-0.7998-1.415-0.44434-2.3037-1.1279-0.88184-0.69043-0.88184-1.8867 0-1.2031 0.9707-1.9756 0.97754-0.77929 2.5566-0.77929 1.6885 0 2.6387 0.90234 0.9502 0.89551 0.9502 2.1396h-1.7227q0-0.73145-0.45801-1.1963-0.45801-0.47168-1.4287-0.47168-0.90918 0-1.3467 0.39648-0.43066 0.38965-0.43066 0.97754 0 0.56055 0.51953 0.92969 0.51953 0.36231 1.5654 0.66992 1.6885 0.49219 2.4951 1.2305 0.81348 0.73828 0.81348 1.9277 0 1.2578-0.96387 1.9892-0.95703 0.73145-2.5771 0.73145-0.95703 0-1.8525-0.34863-0.88867-0.34864-1.4697-1.0459-0.57422-0.69727-0.57422-1.75h1.7295q0 0.94336 0.62207 1.3604 0.62891 0.41699 1.5449 0.41699 0.90234 0 1.3535-0.36914 0.45801-0.36914 0.45801-0.9707z" fill="#9d9d9d"/>
</g>
<g transform="translate(102.9 -23.739)" aria-label="W">
<path d="m15.565 85.047-2.29 9.9531h-1.6611l-1.8936-7.2598-1.9346 7.2598h-1.6543l-2.2969-9.9531h1.7158l1.5449 7.458 1.9004-7.458h1.4492l1.8799 7.4854 1.5244-7.4854z" fill="#9d9d9d"/>
</g>
</g>
<path d="m243.42 22.912c0.55041 0.04588 0.95469 0.53328 0.90932 1.0837l-0.787 9.0016c-0.0453 0.5503-0.52969 0.95998-1.0802 0.91406-0.55041-0.04594-0.95071-0.53668-0.90436-1.0871l0.57622-6.5879-21.769 18.266-1.2856-1.5321 21.769-18.266-6.563-0.54696c-0.55031-0.04588-0.95947-0.5291-0.91363-1.0795 0.0459-0.55038 0.52985-0.95982 1.0803-0.91391zm-100.85 84.258-1.4251-1.6983-1.5321 1.2856 4.1357 4.9287 1.5321-1.2856-1.4251-1.6983 22.935-19.244-1.2856-1.5321z" fill="#000"/>
<g transform="translate(102.9 -23.739)" fill="#000" aria-label="8.1m/s">
<path d="m85.406 85.348q0 2.3789-1.6055 3.6328-1.5938 1.2539-3.9727 1.2539t-3.9961-1.2539q-1.6055-1.2656-1.6055-3.6328 0-1.418 0.73828-2.4961 0.75-1.0781 2.0156-1.6523-1.1016-0.5625-1.7461-1.5469-0.63281-0.98438-0.63281-2.2266 0-2.2617 1.4648-3.4922 1.4766-1.2305 3.75-1.2305 2.2734 0 3.7383 1.2305 1.4766 1.2305 1.4766 3.4922 0 1.2422-0.64453 2.2266-0.64453 0.98438-1.7461 1.5469 1.2656 0.58594 2.0156 1.6641 0.75 1.0664 0.75 2.4844zm-3.2227-7.8164q0-1.125-0.64453-1.8281-0.63281-0.71484-1.7227-0.71484-1.0781 0-1.7227 0.69141-0.64453 0.67969-0.64453 1.8516 0 1.1484 0.63281 1.8516 0.64453 0.70312 1.7461 0.70312 1.0898 0 1.7227-0.70312 0.63281-0.70312 0.63281-1.8516zm0.375 7.6641q0-1.2656-0.76172-2.0391-0.75-0.78516-1.9922-0.78516-1.2539 0-1.9922 0.78516-0.73828 0.77344-0.73828 2.0391 0 1.2891 0.73828 2.0273 0.73828 0.72656 2.0156 0.72656t2.0039-0.72656q0.72656-0.73828 0.72656-2.0273z"/>
<path d="m88.242 88.57q0-0.66797 0.43359-1.125 0.43359-0.45703 1.2188-0.45703t1.2188 0.45703q0.44531 0.45703 0.44531 1.125 0 0.66797-0.44531 1.125-0.43359 0.44531-1.2188 0.44531t-1.2188-0.44531q-0.43359-0.45703-0.43359-1.125z"/>
<path d="m102.35 72.879v17.121h-2.8477v-13.711l-4.1836 1.4297v-2.4023l6.668-2.4375z"/>
<path d="m81.448 102.02q-1.0049 0-1.4219 0.82032v5.3594h-1.6543v-7.3965h1.5586l0.04785 0.77246q0.78613-0.90918 2.1396-0.90918 1.4697 0 2.0234 1.1279 0.81348-1.1279 2.2764-1.1279 1.1279 0 1.7773 0.6289 0.64942 0.62891 0.65625 2.0781v4.8262h-1.6611v-4.7988q0-0.82031-0.3623-1.1006-0.35547-0.28028-0.9707-0.28028-0.56738 0-0.92969 0.30762-0.35547 0.30762-0.49219 0.79981v5.0723h-1.6611v-4.7852q0-0.77929-0.35547-1.0869-0.35547-0.30762-0.9707-0.30762z"/>
<path d="m94.949 98.247-3.917 10.808h-1.3125l3.9238-10.808z"/>
<path d="m100.25 106.19q0-0.35547-0.29395-0.61524-0.28711-0.2666-1.2578-0.47168-1.2031-0.25293-1.9482-0.75195-0.73828-0.50586-0.73828-1.4424 0-0.90918 0.76562-1.5723 0.76562-0.66992 2.085-0.66992 1.3877 0 2.1738 0.67676 0.79297 0.67675 0.79297 1.6748h-1.6611q0-0.43066-0.32813-0.76562-0.32812-0.3418-0.97754-0.3418-0.62891 0-0.92969 0.28027-0.30078 0.28028-0.30078 0.64942 0 0.35546 0.30078 0.57421 0.30078 0.21875 1.2168 0.417 1.2852 0.28027 2.0029 0.77929 0.72461 0.49219 0.72461 1.4834 0 0.98438-0.81348 1.6133-0.81347 0.62891-2.1738 0.62891-1.5244 0-2.3242-0.7793-0.7998-0.77929-0.7998-1.6953h1.6133q0.02734 0.68359 0.49902 0.95703 0.47852 0.27344 1.0254 0.27344 0.65625 0 0.99805-0.25293 0.34864-0.25293 0.34864-0.64941z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

153
ui-ngx/src/assets/widget/wind-speed-direction/simplified-layout.svg

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="134" height="133" fill="none" version="1.1" viewBox="0 0 134 133" xmlns="http://www.w3.org/2000/svg">
<rect x="8.8335" y="4.5" width="117" height="117" rx="1.7594" fill="#fff" filter="url(#filter0_d_3060_521241)"/>
<defs>
<filter id="filter0_d_3060_521241" x=".8335" y=".5" width="133" height="133" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3060_521241"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow_3060_521241" result="shape"/>
</filter>
</defs>
<g transform="matrix(.44244 0 0 .44244 -18.017 33.104)" fill="#000" stroke="#000" stroke-opacity=".11765" stroke-width="2">
<line x1="242.4" x2="236.74" y1="16.763" y2="22.42"/>
<line x1="242.4" x2="236.74" y1="115.76" y2="110.1"/>
<line x1="143.4" x2="149.06" y1="115.76" y2="110.1"/>
<line x1="143.4" x2="149.06" y1="16.763" y2="22.42"/>
</g>
<g transform="matrix(.44244 0 0 .44244 27.511 22.601)" fill="#000" stroke="#000" stroke-opacity=".12" stroke-width="1.2">
<line transform="rotate(3,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(6,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(9,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(12,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(15,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(18,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(21,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(24,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(27,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(30,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(33,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(36,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(39,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(42,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(48,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(51,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(54,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(57,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(60,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(63,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(66,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(69,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(72,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(75,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(78,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(81,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(84,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(87,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(93,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(96,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(99,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(102,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(105,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(108,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(111,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(114,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(117,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(120,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(123,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(126,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(129,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(132,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(138,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(141,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(144,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(147,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(150,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(153,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(156,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(159,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(162,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(165,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(168,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(171,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(174,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(177,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(183 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(186 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(189 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(192 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(195 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(198 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(201 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(204 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(207 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(210 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(213 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(216 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(219 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(222 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(228 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(231 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(234 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(237 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(240 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(243 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(246 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(249 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(252 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(255 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(258 90 90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-99,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-96,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-93,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-87,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-84,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-81,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-78,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-75,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-72,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-69,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-66,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-63,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-60,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-57,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-54,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-51,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-48,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-42,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-39,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-36,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-33,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-30,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-27,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-24,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-21,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-18,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-15,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-12,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-9,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-6,90,90)" x1="90" x2="90" y1="20" y2="23"/>
<line transform="rotate(-3,90,90)" x1="90" x2="90" y1="20" y2="23"/>
</g>
<g fill="#9e9e9e" stroke="#9e9e9e" stroke-width=".88487">
<line x1="98.301" x2="94.762" y1="62.42" y2="62.42"/>
<line x1="67.331" x2="67.331" y1="93.391" y2="89.851"/>
<line x1="36.36" x2="39.9" y1="62.42" y2="62.42"/>
</g>
<g>
<path d="m66.955 31.658c0.1733-0.27726 0.57707-0.27726 0.75037 0l1.4139 2.2623c0.18414 0.29468-0.0277 0.67693-0.37519 0.67693h-2.8279c-0.34749 0-0.55933-0.38224-0.37519-0.67693z" fill="#9d9d9d" stroke-width=".44244"/>
<path d="m89.681 43.241c0.24352 0.0203 0.42239 0.23594 0.40232 0.47946l-0.3482 3.9826c-0.02004 0.24348-0.23435 0.42473-0.4779 0.40442-0.24352-0.02032-0.42063-0.23745-0.40012-0.48098l0.25494-2.9147-9.6313 8.0816-0.56879-0.67785 9.6313-8.0816-2.9037-0.242c-0.24348-0.0203-0.4245-0.2341-0.40422-0.4776 0.02031-0.24351 0.23442-0.42466 0.47796-0.40435zm-44.62 37.279-0.6305-0.7514-0.67785 0.56878 1.8298 2.1807 0.67785-0.56878-0.6305-0.7514 10.147-8.5144-0.56878-0.67785z" fill="#000" stroke-width=".44244"/>
<g transform="matrix(.44244 0 0 .44244 27.511 22.601)" fill="#000" aria-label="8.1m/s">
<path d="m85.406 85.348q0 2.3789-1.6055 3.6328-1.5938 1.2539-3.9727 1.2539t-3.9961-1.2539q-1.6055-1.2656-1.6055-3.6328 0-1.418 0.73828-2.4961 0.75-1.0781 2.0156-1.6523-1.1016-0.5625-1.7461-1.5469-0.63281-0.98438-0.63281-2.2266 0-2.2617 1.4648-3.4922 1.4766-1.2305 3.75-1.2305 2.2734 0 3.7383 1.2305 1.4766 1.2305 1.4766 3.4922 0 1.2422-0.64453 2.2266-0.64453 0.98438-1.7461 1.5469 1.2656 0.58594 2.0156 1.6641 0.75 1.0664 0.75 2.4844zm-3.2227-7.8164q0-1.125-0.64453-1.8281-0.63281-0.71484-1.7227-0.71484-1.0781 0-1.7227 0.69141-0.64453 0.67969-0.64453 1.8516 0 1.1484 0.63281 1.8516 0.64453 0.70312 1.7461 0.70312 1.0898 0 1.7227-0.70312 0.63281-0.70312 0.63281-1.8516zm0.375 7.6641q0-1.2656-0.76172-2.0391-0.75-0.78516-1.9922-0.78516-1.2539 0-1.9922 0.78516-0.73828 0.77344-0.73828 2.0391 0 1.2891 0.73828 2.0273 0.73828 0.72656 2.0156 0.72656t2.0039-0.72656q0.72656-0.73828 0.72656-2.0273z"/>
<path d="m88.242 88.57q0-0.66797 0.43359-1.125 0.43359-0.45703 1.2188-0.45703t1.2188 0.45703q0.44531 0.45703 0.44531 1.125 0 0.66797-0.44531 1.125-0.43359 0.44531-1.2188 0.44531t-1.2188-0.44531q-0.43359-0.45703-0.43359-1.125z"/>
<path d="m102.35 72.879v17.121h-2.8477v-13.711l-4.1836 1.4297v-2.4023l6.668-2.4375z"/>
<path d="m81.448 102.02q-1.0049 0-1.4219 0.82032v5.3594h-1.6543v-7.3965h1.5586l0.04785 0.77246q0.78613-0.90918 2.1396-0.90918 1.4697 0 2.0234 1.1279 0.81348-1.1279 2.2764-1.1279 1.1279 0 1.7773 0.6289 0.64942 0.62891 0.65625 2.0781v4.8262h-1.6611v-4.7988q0-0.82031-0.3623-1.1006-0.35547-0.28028-0.9707-0.28028-0.56738 0-0.92969 0.30762-0.35547 0.30762-0.49219 0.79981v5.0723h-1.6611v-4.7852q0-0.77929-0.35547-1.0869-0.35547-0.30762-0.9707-0.30762z"/>
<path d="m94.949 98.247-3.917 10.808h-1.3125l3.9238-10.808z"/>
<path d="m100.25 106.19q0-0.35547-0.29395-0.61524-0.28711-0.2666-1.2578-0.47168-1.2031-0.25293-1.9482-0.75195-0.73828-0.50586-0.73828-1.4424 0-0.90918 0.76562-1.5723 0.76562-0.66992 2.085-0.66992 1.3877 0 2.1738 0.67676 0.79297 0.67675 0.79297 1.6748h-1.6611q0-0.43066-0.32813-0.76562-0.32812-0.3418-0.97754-0.3418-0.62891 0-0.92969 0.28027-0.30078 0.28028-0.30078 0.64942 0 0.35546 0.30078 0.57421 0.30078 0.21875 1.2168 0.417 1.2852 0.28027 2.0029 0.77929 0.72461 0.49219 0.72461 1.4834 0 0.98438-0.81348 1.6133-0.81347 0.62891-2.1738 0.62891-1.5244 0-2.3242-0.7793-0.7998-0.77929-0.7998-1.6953h1.6133q0.02734 0.68359 0.49902 0.95703 0.47852 0.27344 1.0254 0.27344 0.65625 0 0.99805-0.25293 0.34864-0.25293 0.34864-0.64941z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

12
ui-ngx/src/form.scss

@ -204,6 +204,12 @@
.mat-slide:only-child {
margin: 8px 0;
}
.tb-required::after {
font-size: 13px;
color: rgba(0, 0, 0, .54);
vertical-align: top;
content: " *";
}
}
.tb-form-panel, .tb-form-row {
@ -227,8 +233,10 @@
&:before {
opacity: 0;
}
.mdc-line-ripple::before {
border-bottom-color: rgba(0, 0, 0, 0.12);
&:not(.mdc-text-field--invalid) {
.mdc-line-ripple::before {
border-bottom-color: rgba(0, 0, 0, 0.12);
}
}
}
.mat-mdc-form-field-focus-overlay {

5
ui-ngx/yarn.lock

@ -2747,6 +2747,11 @@
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@svgdotjs/svg.js@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz#6baa8cef6778a93818ac18faa2055222e60aa644"
integrity sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==
"@tinymce/tinymce-angular@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-7.0.0.tgz#010de497d5774a8bdc5d5936bf4fb976adf05f56"

Loading…
Cancel
Save