Merge remote-tracking branch 'upstream/master' into improvement/gateway-dashboard

This commit is contained in:
Vladyslav_Prykhodko 2024-07-08 17:12:40 +03:00
commit 34f6b84bca
65 changed files with 1673 additions and 353 deletions

File diff suppressed because one or more lines are too long

View File

@ -23,6 +23,7 @@
"cards.html_card",
"cards.html_value_card",
"cards.markdown_card",
"cards.simple_card"
"cards.simple_card",
"unread_notifications"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
"name": "Bars",
"deprecated": true,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA8FBMVEUhlvNMr1Bqamp5eXl7e3t8fHx9fX1+fn5/f3+AgICCgoKDg4OEhISGhoaHh4eKioqMjIyNjY2Ojo6QkJCRkZGSkpKWlpaXl5ebm5udnZ2enp6goKChoaGkpKSnp6epqamsrKyurq6xsbGzs7O1tbW2tra3t7e4uLi7u7u9vb3BwcHCwsLDw8PGxsbKysrNzc3Ozs7R0dHS0tLT09PZ2dna2trc3Nzd3d3e3t7g4ODh4eHj4+Pk5OTm5ubn5+fo6Ojp6enu7u7w8PDz8/P0Qzb09PT29vb39/f5+fn6+vr7+/v8/Pz9/f3+/v7/wQf///+dc+aLAAAAAWJLR0RPbmZBSQAAAcFJREFUeNrt3ds2AgEYhuHsaSOZbAvZi0r2YYjCJOW7/7txZhkcDNbM6h/vdwfPmlX/ybtmEorJErGCeJLadz3rkKPpZamaLTp925DHdFvSpKelU9uQ/cLKhtcdk7YqtiHruevtojch7ZZtQ0o1dcdfRqXNqm1IbVU3OaVamm/YhvQW5zIXOknnC5JUt7qEpE5fUv/5HVePy2UHAgQIECBAgAABAgQIECBxgrwGHBAgQIAAAQIECBAgQIAAAQIECJC/QRIBN0iQ+66voDMLuRp2fQWdVUhvNun6CjqrkJ0Dx/UVdEYhzXzfcX0FnVFIrlSZ2mx/LOiMQuqHh6k972NBZ/fv13G/KeiCQkIu4358EL8UdBafSGwuOxAgQIAAAQIECJDYQB4CDggQIECAAAECBAgQIECAAAECBAgQIECA/BrSufn0DjqjkEZmLXkWaUEXEmThXMeFSAu68H4j5b1IC7rQILfZTqQFXViQ1nRTL1EWdCFBnmYuJUVZ0IUEWR1xHKcXZUEXFDLwBR2XHQgQIECAAAEC5H9ChgIOCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgxiBmv+L6Bl9pkxYph15gAAAAAElFTkSuQmCC",
"description": "Displays latest values of the attributes or time-series data for multiple entities as separate bars.",
"description": "Displays latest values of the attributes or time series data for multiple entities as separate bars.",
"descriptor": {
"type": "latest",
"sizeX": 7,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
"name": "Doughnut",
"deprecated": false,
"image": "data:image/png;base64,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",
"description": "Displays the latest values of the attributes or time-series data in a doughnut chart. Supports numeric values only.",
"description": "Displays the latest values of the attributes or time series data in a doughnut chart. Supports numeric values only.",
"descriptor": {
"type": "latest",
"sizeX": 4,

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
"name": "Horizontal doughnut",
"deprecated": false,
"image": "data:image/png;base64,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",
"description": "Displays the latest values of the attributes or time-series data in a doughnut chart using horizontal layout. Supports numeric values only.",
"description": "Displays the latest values of the attributes or time series data in a doughnut chart using horizontal layout. Supports numeric values only.",
"descriptor": {
"type": "latest",
"sizeX": 4,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
"name": "Pie",
"deprecated": false,
"image": "tb-image:cGllX2NoYXJ0LnN2Zw==:IlBpZSIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAxIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDUzOF8zNTMwNikiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjIuMzg4MDYiIGZpbGw9IndoaXRlIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPHBhdGggZD0iTTE0OC45NzUgMTI5LjQ5N0MxNTYuNzc0IDEyMS42OTggMTYyLjYyIDExMi4xNjYgMTY2LjAzNiAxMDEuNjc4TDk5LjQ3NzUgODBMMTQ4Ljk3NSAxMjkuNDk3WiIgZmlsbD0iI0ZGREUzMCIgc3Ryb2tlPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOTkuNDc3NSAxMEM4Ny45NjYgMTAgNzYuNjMyMSAxMi44MzkgNjYuNDc5OCAxOC4yNjU1QzU2LjMyNzUgMjMuNjkyIDQ3LjY3MDIgMzEuNTM4NiA0MS4yNzQ3IDQxLjExMDFDMzQuODc5MiA1MC42ODE2IDMwLjk0MjkgNjEuNjgyNyAyOS44MTQ2IDczLjEzODhDMjguNjg2MyA4NC41OTQ5IDMwLjQwMDcgOTYuMTUyNSAzNC44MDYgMTA2Ljc4OEMzOS4yMTEzIDExNy40MjMgNDYuMTcxNCAxMjYuODA4IDU1LjA3IDEzNC4xMTFDNjMuOTY4NiAxNDEuNDE0IDc0LjUzMDggMTQ2LjQwOSA4NS44MjEyIDE0OC42NTVDOTcuMTExNiAxNTAuOTAxIDEwOC43ODIgMTUwLjMyNyAxMTkuNzk3IDE0Ni45ODZDMTMwLjgxMyAxNDMuNjQ0IDE0MC44MzUgMTM3LjYzNyAxNDguOTc1IDEyOS40OTdMOTkuNDc3NSA4MFYxMFoiIGZpbGw9IiMwODg3MkIiIHN0cm9rZT0id2hpdGUiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTM4XzM1MzA2KSI+CjxwYXRoIGQ9Ik0xNjcuNTYyIDEwMi4wNTdDMTcxLjAxNCA5MS4yNTc1IDE3MS44ODIgNzkuNzg5OCAxNzAuMDk2IDY4LjU4ODVDMTY4LjMxMSA1Ny4zODcxIDE2My45MjIgNDYuNzY5MiAxNTcuMjg4IDM3LjYwMDJDMTUwLjY1MyAyOC40MzEyIDE0MS45NjEgMjAuOTcwNiAxMzEuOTIgMTUuODI2NkMxMjEuODc5IDEwLjY4MjYgMTEwLjc3MiA4LjAwMDc4IDk5LjUwNjQgNy45OTk3Nkw5OS41IDc5Ljk5OThMMTY3LjU2MiAxMDIuMDU3WiIgZmlsbD0iI0ZGNEQ1QSIvPgo8cGF0aCBkPSJNMTY3LjU2MiAxMDIuMDU3QzE3MS4wMTQgOTEuMjU3NSAxNzEuODgyIDc5Ljc4OTggMTcwLjA5NiA2OC41ODg1QzE2OC4zMTEgNTcuMzg3MSAxNjMuOTIyIDQ2Ljc2OTIgMTU3LjI4OCAzNy42MDAyQzE1MC42NTMgMjguNDMxMiAxNDEuOTYxIDIwLjk3MDYgMTMxLjkyIDE1LjgyNjZDMTIxLjg3OSAxMC42ODI2IDExMC43NzIgOC4wMDA3OCA5OS41MDY0IDcuOTk5NzZMOTkuNSA3OS45OTk4TDE2Ny41NjIgMTAyLjA1N1oiIHN0cm9rZT0id2hpdGUiLz4KPC9nPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDUzOF8zNTMwNiIgeD0iLTcuMTY0MTgiIHk9Ii00Ljc3NjEyIiB3aWR0aD0iMjE0LjMyOCIgaGVpZ2h0PSIxNzQuMzI4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIuMzg4MDYiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMy41ODIwOSIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDYgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzQ1MzhfMzUzMDYiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTM4XzM1MzA2IiB4PSI4NyIgeT0iLTQuNTAwMjQiIHdpZHRoPSI5Ni41IiBoZWlnaHQ9IjExOS4xODciIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjYiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjI1IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDUzOF8zNTMwNiIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=",
"description": "Displays the latest values of the attributes or time-series data in a pie chart. Supports numeric values only.",
"description": "Displays the latest values of the attributes or time series data in a pie chart. Supports numeric values only.",
"descriptor": {
"type": "latest",
"sizeX": 5,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
"name": "Timeseries Bar Chart",
"deprecated": true,
"image": "tb-image:dGltZXNlcmllc19iYXJfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlRpbWVzZXJpZXMgQmFyIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=",
"description": "Displays changes to time-series data over time—for example, daily water consumption for the last month.",
"description": "Displays changes to time series data over time—for example, daily water consumption for the last month.",
"descriptor": {
"type": "timeseries",
"sizeX": 8,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.63.0</grpc.version>
<tbel.version>1.2.0</tbel.version>
<tbel.version>1.2.1</tbel.version>
<lombok.version>1.18.32</lombok.version>
<paho.client.version>1.2.5</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

View File

@ -44,7 +44,11 @@
</section>
</ng-container>
<ng-template #emptyNotification>
<img src="assets/notification-bell.svg" alt="empty notification" style="margin: 20px 24%">
<div class="tb-no-notification-svg-color" style="margin: 20px 24%">
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%">
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use>
</svg>
</div>
<span style="text-align: center; margin-bottom: 12px" translate>notification.no-notifications-yet</span>
</ng-template>
<ng-template #loadingNotification>

View File

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../scss/constants';
.tb-no-notification-svg-color {
color: $tb-primary-color;
}

View File

@ -29,7 +29,7 @@ import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.model
@Component({
selector: 'tb-show-notification-popover',
templateUrl: './show-notification-popover.component.html',
styleUrls: []
styleUrls: ['show-notification-popover.component.scss']
})
export class ShowNotificationPopoverComponent extends PageComponent implements OnDestroy, OnInit {

View File

@ -139,6 +139,9 @@ import {
import {
LabelValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/label-value-card-basic-config.component';
import {
UnreadNotificationBasicConfigComponent
} from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component';
@NgModule({
declarations: [
@ -185,7 +188,8 @@ import {
DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent
LabelValueCardBasicConfigComponent,
UnreadNotificationBasicConfigComponent
],
imports: [
CommonModule,
@ -234,7 +238,8 @@ import {
DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent
LabelValueCardBasicConfigComponent,
UnreadNotificationBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -277,5 +282,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-digital-simple-gauge-basic-config': DigitalSimpleGaugeBasicConfigComponent,
'tb-mobile-app-qr-code-basic-config': MobileAppQrCodeBasicConfigComponent,
'tb-label-card-basic-config': LabelCardBasicConfigComponent,
'tb-label-value-card-basic-config': LabelValueCardBasicConfigComponent
'tb-label-value-card-basic-config': LabelValueCardBasicConfigComponent,
'tb-unread-notification-basic-config': UnreadNotificationBasicConfigComponent
};

View File

@ -0,0 +1,128 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="unreadNotificationWidgetConfigForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<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]="unreadNotificationWidgetConfigForm.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.notification.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]="unreadNotificationWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.max-notification-display' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxNotificationDisplay" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-row no-padding no-border">
<mat-slide-toggle class="mat-slide" formControlName="showCounter">
{{ 'widgets.notification.counter' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.counter-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="counterValueFont"
clearButton
[previewText]="unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value.toString()"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="counterValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.counter-color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="counterColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.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="viewAll">{{ 'widgets.notification.button-view-all' | translate }}</mat-chip-option>
<mat-chip-option value="filter">{{ 'widgets.notification.button-filter' | translate }}</mat-chip-option>
<mat-chip-option value="markAsRead">{{ 'widgets.notification.button-mark-read' | translate }}</mat-chip-option>
<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 class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

View File

@ -0,0 +1,177 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } 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 { WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models';
import {
unreadNotificationDefaultSettings,
UnreadNotificationWidgetSettings
} from '@home/components/widget/lib/cards/unread-notification-widget.models';
@Component({
selector: 'tb-unread-notification-basic-config',
templateUrl: './unread-notification-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigComponent {
unreadNotificationWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.unreadNotificationWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const iconSize = resolveCssSize(configData.config.iconSize);
const settings: UnreadNotificationWidgetSettings = {...unreadNotificationDefaultSettings, ...(configData.config.settings || {})};
this.unreadNotificationWidgetConfigForm = this.fb.group({
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, []],
maxNotificationDisplay: [settings.maxNotificationDisplay, [Validators.required, Validators.min(1)]],
showCounter: [settings.showCounter, []],
counterValueFont: [settings.counterValueFont, []],
counterValueColor: [settings.counterValueColor, []],
counterColor: [settings.counterColor, []],
background: [settings.background, []],
padding: [settings.padding, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected validatorTriggers(): string[] {
return ['showCounter', 'showTitle', 'showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showCounter: boolean = this.unreadNotificationWidgetConfigForm.get('showCounter').value;
const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.unreadNotificationWidgetConfigForm.get('showIcon').value;
if (showTitle) {
this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent});
}
if (showIcon) {
this.unreadNotificationWidgetConfigForm.get('iconSize').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('icon').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('iconSize').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('icon').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconColor').disable({emitEvent});
}
if (showCounter) {
this.unreadNotificationWidgetConfigForm.get('counterValueFont').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterValueColor').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('counterValueFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent});
}
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
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.maxNotificationDisplay = config.maxNotificationDisplay;
this.widgetConfig.config.settings.showCounter = config.showCounter;
this.widgetConfig.config.settings.counterValueFont = config.counterValueFont;
this.widgetConfig.config.settings.counterValueColor = config.counterValueColor;
this.widgetConfig.config.settings.counterColor = config.counterColor;
this.widgetConfig.config.settings.background = config.background;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
return this.widgetConfig;
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.settings?.enableViewAll) || config.settings?.enableViewAll) {
buttons.push('viewAll');
}
if (isUndefined(config.settings?.enableFilter) || config.settings?.enableFilter) {
buttons.push('filter');
}
if (isUndefined(config.settings?.enableMarkAsRead) || config.settings?.enableMarkAsRead) {
buttons.push('markAsRead');
}
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.settings.enableViewAll = buttons.includes('viewAll');
config.settings.enableFilter = buttons.includes('filter');
config.settings.enableMarkAsRead = buttons.includes('markAsRead');
config.enableFullscreen = buttons.includes('fullscreen');
}
}

View File

@ -46,12 +46,12 @@ import {
TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import {
TimeSeriesChartTooltipTrigger,
TimeSeriesChartKeySettings,
TimeSeriesChartType,
TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { TimeSeriesChartTooltipTrigger } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
@Component({
selector: 'tb-time-series-chart-basic-config',

View File

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form fxLayout="column" fxFlex class="mat-content mat-padding" (ngSubmit)="update()">
<div class="tb-form-panel no-padding">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>widgets.notification.notification-types</div>
<mat-form-field floatLabel="auto" appearance="outline" subscriptSizing="dynamic" class="flex fb-chips">
<mat-chip-grid #chipList>
<mat-chip-row
*ngFor="let type of selectedNotificationTypes"
removable
(removed)="remove(type)">
{{ notificationTypesTranslateMap.get(type).name | translate }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text"
placeholder="{{ ( selectedNotificationTypes.length ? 'widgets.notification.notification-type' : 'widgets.notification.any-type' ) | translate }}"
style="max-width: 200px;"
[formControl]="searchControlName"
#notificationTypeInput
(focusin)="onFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="NotificationTypeAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="chipAdd($event)">
</mat-chip-grid>
<mat-autocomplete #NotificationTypeAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="selected($event)"
[displayWith]="displayTypeFn">
<mat-option *ngFor="let type of filteredNotificationTypesList | async" [value]="type">
<span [innerHTML]="notificationTypesTranslateMap.get(type).name | translate | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="reset()"
color="primary">
{{ 'action.reset' | translate }}
</button>
<span fxFlex></span>
<button type="button"
(click)="cancel()"
mat-button>
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary">
{{ 'action.update' | translate }}
</button>
</div>
</form>

View File

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: flex;
width: 100%;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}

View File

@ -0,0 +1,140 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Inject, InjectionToken, OnInit, ViewChild } from '@angular/core';
import { NotificationTemplateTypeTranslateMap, NotificationType } from '@shared/models/notification.models';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { Observable } from 'rxjs';
import { FormControl } from '@angular/forms';
import { debounceTime, map } from 'rxjs/operators';
import { OverlayRef } from '@angular/cdk/overlay';
export const NOTIFICATION_TYPE_FILTER_PANEL_DATA = new InjectionToken<any>('NotificationTypeFilterPanelData');
export interface NotificationTypeFilterPanelData {
notificationTypes: Array<NotificationType>;
notificationTypesUpdated: (notificationTypes: Array<NotificationType>) => void;
}
@Component({
selector: 'tb-notification-type-filter-panel',
templateUrl: './notification-type-filter-panel.component.html',
styleUrls: ['notification-type-filter-panel.component.scss']
})
export class NotificationTypeFilterPanelComponent implements OnInit{
@ViewChild('searchInput') searchInputField: ElementRef;
searchText = '';
searchControlName = new FormControl('');
filteredNotificationTypesList: Observable<Array<NotificationType>>;
selectedNotificationTypes: Array<NotificationType> = [];
notificationTypesTranslateMap = NotificationTemplateTypeTranslateMap;
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
private notificationType = NotificationType;
private notificationTypes = Object.keys(NotificationType) as Array<NotificationType>;
private dirty = false;
@ViewChild('notificationTypeInput') notificationTypeInput: ElementRef<HTMLInputElement>;
constructor(@Inject(NOTIFICATION_TYPE_FILTER_PANEL_DATA) public data: NotificationTypeFilterPanelData,
private overlayRef: OverlayRef) {
this.selectedNotificationTypes = this.data.notificationTypes;
this.dirty = true;
}
ngOnInit() {
this.filteredNotificationTypesList = this.searchControlName.valueChanges.pipe(
debounceTime(150),
map(value => {
this.searchText = value;
return this.notificationTypes.filter(type => !this.selectedNotificationTypes.includes(type))
.filter(type => value ? type.toUpperCase().startsWith(value.toUpperCase()) : true);
})
);
}
public update() {
this.data.notificationTypesUpdated(this.selectedNotificationTypes);
if (this.overlayRef) {
this.overlayRef.dispose();
}
}
cancel() {
if (this.overlayRef) {
this.overlayRef.dispose();
}
}
public reset() {
this.selectedNotificationTypes.length = 0;
this.searchControlName.updateValueAndValidity({emitEvent: true});
}
remove(type: NotificationType) {
const index = this.selectedNotificationTypes.indexOf(type);
if (index >= 0) {
this.selectedNotificationTypes.splice(index, 1);
this.searchControlName.updateValueAndValidity({emitEvent: true});
}
}
onFocus() {
if (this.dirty) {
this.searchControlName.updateValueAndValidity({emitEvent: true});
this.dirty = false;
}
}
private add(type: NotificationType): void {
this.selectedNotificationTypes.push(type);
}
chipAdd(event: MatChipInputEvent): void {
const value = (event.value || '').trim();
if (value && this.notificationType[value]) {
this.add(this.notificationType[value]);
this.clear('');
}
}
selected(event: MatAutocompleteSelectedEvent): void {
if (this.notificationType[event.option.value]) {
this.add(this.notificationType[event.option.value]);
}
this.clear('');
}
clear(value: string = '') {
this.notificationTypeInput.nativeElement.value = value;
this.searchControlName.patchValue(value, {emitEvent: true});
setTimeout(() => {
this.notificationTypeInput.nativeElement.blur();
this.notificationTypeInput.nativeElement.focus();
}, 0);
}
displayTypeFn(type?: string): string | undefined {
return type ? type : undefined;
}
}

View File

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-unread-notification-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-unread-notification-overlay" [style]="overlayStyle"></div>
<ng-template #counter>
<div *ngIf="showCounter" class="notification-counter" [style.background-color]="counterBackground">
<span class="notification-counter-value" [style]="counterValueStyle">{{ count$ | async }}</span>
</div>
</ng-template>
<ng-container *ngTemplateOutlet="widgetTitlePanel; context:{ titleSuffixTemplate: counter }"></ng-container>
<div class="tb-unread-notification-content">
<ng-container *ngIf="loadNotification; else loadingNotification">
<div *ngIf="notifications.length; else emptyNotification" style="overflow: auto; width: 100%;">
<section style="min-height: 100px; overflow: auto; padding: 6px 0;">
<div *ngFor="let notification of notifications; let last = last; trackBy: trackById">
<tb-notification [notification]="notification"
(markAsRead)="markAsRead($event)">
</tb-notification>
</div>
</section>
</div>
</ng-container>
<ng-template #emptyNotification>
<div class="tb-no-notification-svg-color" style="height: 85%;">
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%">
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use>
</svg>
</div>
<span class="tb-no-notification-text" translate>notification.no-notifications-yet</span>
</ng-template>
<ng-template #loadingNotification>
<div class="tb-no-data-available" style="margin: 20px; gap: 16px;">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<div class="tb-no-data-text" translate>notification.loading-notifications</div>
</div>
</ng-template>
</div>
</div>

View File

@ -0,0 +1,65 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../../../scss/constants";
.tb-no-notification-svg-color {
color: $tb-primary-color;
}
.tb-unread-notification-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px 24px 24px 24px;
> div:not(.tb-unread-notification-overlay) {
z-index: 1;
}
div.tb-widget-title {
padding: 0;
}
.tb-unread-notification-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-unread-notification-content {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
.tb-no-notification-text {
text-align: center;
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.38);
}
}
.notification-counter {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 22px;
background-color: green;
border-radius: 7px;
}
}

View File

@ -0,0 +1,284 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
Injector,
Input,
NgZone,
OnDestroy,
OnInit,
StaticProvider,
TemplateRef,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models';
import { isDefined } from '@core/utils';
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { BehaviorSubject, fromEvent, Observable, ReplaySubject, Subscription } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import {
unreadNotificationDefaultSettings,
UnreadNotificationWidgetSettings
} from '@home/components/widget/lib/cards/unread-notification-widget.models';
import { Notification, NotificationRequest, NotificationType } from '@shared/models/notification.models';
import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
import { distinctUntilChanged, map, share, skip, take, tap } from 'rxjs/operators';
import { Router } from '@angular/router';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { ComponentPortal } from '@angular/cdk/portal';
import {
NOTIFICATION_TYPE_FILTER_PANEL_DATA,
NotificationTypeFilterPanelComponent
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { selectUserDetails } from '@core/auth/auth.selectors';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-unread-notification-widget',
templateUrl: './unread-notification-widget.component.html',
styleUrls: ['unread-notification-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy {
settings: UnreadNotificationWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
showCounter = true;
counterValueStyle: ComponentStyle;
counterBackground: string;
notifications: Notification[];
loadNotification = false;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
private counterValue: BehaviorSubject<number> = new BehaviorSubject(0);
count$ = this.counterValue.asObservable().pipe(
distinctUntilChanged(),
map((value) => value >= 100 ? '99+' : value),
tap(() => Promise.resolve().then(() => this.cd.markForCheck())),
share({
connector: () => new ReplaySubject(1)
})
);
private notificationTypes: Array<NotificationType> = [];
private notificationSubscriber: NotificationSubscriber;
private notificationCountSubscriber: Subscription;
private notification: Subscription;
private contentResize$: ResizeObserver;
private defaultDashboardFullscreen = false;
private viewAllAction: WidgetAction = {
name: 'widgets.notification.button-view-all',
show: !this.defaultDashboardFullscreen,
icon: 'open_in_new',
onAction: ($event) => {
this.viewAll($event);
}
};
private filterAction: WidgetAction = {
name: 'widgets.notification.button-filter',
show: true,
icon: 'filter_list',
onAction: ($event) => {
this.editNotificationTypeFilter($event);
}
};
private markAsReadAction: WidgetAction = {
name: 'widgets.notification.button-mark-read',
show: true,
icon: 'done_all',
onAction: ($event) => {
this.markAsAllRead($event);
}
};
constructor(private store: Store<AppState>,
private imagePipe: ImagePipe,
private notificationWsService: NotificationWebsocketService,
private sanitizer: DomSanitizer,
private router: Router,
private zone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.unreadNotificationWidget = this;
this.settings = {...unreadNotificationDefaultSettings, ...this.ctx.settings};
this.showCounter = this.settings.showCounter;
this.counterValueStyle = textStyle(this.settings.counterValueFont);
this.counterValueStyle.color = this.settings.counterValueColor;
this.counterBackground = this.settings.counterColor;
this.ctx.widgetActions = [this.viewAllAction, this.filterAction, this.markAsReadAction];
this.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true;
this.store.pipe(select(selectUserDetails), take(1)).subscribe(
user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen
);
this.filterAction.show = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true;
this.markAsReadAction.show = isDefined(this.settings.enableMarkAsRead) ? this.settings.enableMarkAsRead : true;
this.initSubscription();
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
}
ngOnDestroy() {
if (this.contentResize$) {
this.contentResize$.disconnect();
}
this.unsubscribeSubscription();
}
private initSubscription() {
this.notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(
this.notificationWsService, this.zone, this.settings.maxNotificationDisplay, this.notificationTypes);
this.notification = this.notificationSubscriber.notifications$.subscribe(value => {
if (Array.isArray(value)) {
this.loadNotification = true;
this.notifications = value;
this.cd.markForCheck();
}
});
this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.pipe(
skip(1),
).subscribe(value => this.counterValue.next(value));
this.notificationSubscriber.subscribe();
}
private unsubscribeSubscription() {
this.notificationSubscriber.unsubscribe();
this.notificationCountSubscriber.unsubscribe();
this.notification.unsubscribe();
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
markAsRead(id: string) {
const cmd = NotificationSubscriber.createMarkAsReadCommand(this.notificationWsService, [id]);
cmd.subscribe();
}
markAsAllRead($event: Event) {
if ($event) {
$event.stopPropagation();
}
const cmd = NotificationSubscriber.createMarkAllAsReadCommand(this.notificationWsService);
cmd.subscribe();
}
viewAll($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {});
}
trackById(index: number, item: NotificationRequest): string {
return item.id.id;
}
private editNotificationTypeFilter($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
height: 'fit-content',
maxHeight: '75vh',
width: '100%',
maxWidth: 700
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions(DEFAULT_OVERLAY_POSITIONS);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: NOTIFICATION_TYPE_FILTER_PANEL_DATA,
useValue: {
notificationTypes: this.notificationTypes,
notificationTypesUpdated: (notificationTypes: Array<NotificationType>) => {
this.notificationTypes = notificationTypes;
this.unsubscribeSubscription();
this.initSubscription();
}
}
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(NotificationTypeFilterPanelComponent,
this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges();
}
}

View File

@ -0,0 +1,59 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models';
export interface UnreadNotificationWidgetSettings {
maxNotificationDisplay: number;
showCounter: boolean;
counterValueFont: Font;
counterValueColor: string;
counterColor: string;
enableViewAll: boolean;
enableFilter: boolean;
enableMarkAsRead: boolean;
background: BackgroundSettings;
padding: string;
}
export const unreadNotificationDefaultSettings: UnreadNotificationWidgetSettings = {
maxNotificationDisplay: 6,
showCounter: true,
counterValueFont: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '600',
lineHeight: ''
},
counterValueColor: '#fff',
counterColor: '#305680',
enableViewAll: true,
enableFilter: true,
enableMarkAsRead: true,
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '12px'
};

View File

@ -35,7 +35,6 @@ import {
TimeSeriesChartSeriesType,
TimeSeriesChartSettings,
TimeSeriesChartThreshold,
TimeSeriesChartTooltipWidgetSettings,
TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
@ -48,6 +47,9 @@ import {
ChartFillSettings,
ChartFillType
} from '@home/components/widget/lib/chart/chart.models';
import {
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export interface BarChartWithLabelsWidgetSettings extends TimeSeriesChartTooltipWidgetSettings {
dataZoom: boolean;

View File

@ -38,7 +38,6 @@ import {
TimeSeriesChartSettings,
TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartTooltipWidgetSettings,
TimeSeriesChartVisualMapPiece,
TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings
@ -54,6 +53,9 @@ import {
ChartLineType,
ChartShape
} from '@home/components/widget/lib/chart/chart.models';
import {
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export interface RangeItem {
index: number;

View File

@ -18,13 +18,15 @@ import {
TimeSeriesChartStateSettings,
TimeSeriesChartStateSourceType,
TimeSeriesChartTicksFormatter,
TimeSeriesChartTicksGenerator,
TimeSeriesChartTooltipValueFormatFunction
TimeSeriesChartTicksGenerator
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { UtilsService } from '@core/services/utils.service';
import { FormattedData } from '@shared/models/widget.models';
import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import { LabelFormatterCallback } from 'echarts';
import {
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export class TimeSeriesChartStateValueConverter {

View File

@ -0,0 +1,263 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { isFunction } from '@core/utils';
import { FormattedData } from '@shared/models/widget.models';
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models';
import {
TimeSeriesChartDataItem,
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { Renderer2, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { Interval } from '@shared/models/time/time.models';
export type TimeSeriesChartTooltipValueFormatFunction =
(value: any, latestData: FormattedData, units?: string, decimals?: number) => string;
export interface TimeSeriesChartTooltipWidgetSettings {
showTooltip: boolean;
tooltipTrigger?: TimeSeriesChartTooltipTrigger;
tooltipShowFocusedSeries?: boolean;
tooltipLabelFont: Font;
tooltipLabelColor: string;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipValueFormatter?: string | TimeSeriesChartTooltipValueFormatFunction;
tooltipShowDate: boolean;
tooltipDateInterval?: boolean;
tooltipDateFormat: DateFormatSettings;
tooltipDateFont: Font;
tooltipDateColor: string;
tooltipBackgroundColor: string;
tooltipBackgroundBlur: number;
}
export enum TimeSeriesChartTooltipTrigger {
point = 'point',
axis = 'axis'
}
export const tooltipTriggerTranslationMap = new Map<TimeSeriesChartTooltipTrigger, string>(
[
[TimeSeriesChartTooltipTrigger.point, 'tooltip.trigger-point'],
[TimeSeriesChartTooltipTrigger.axis, 'tooltip.trigger-axis']
]
);
interface TooltipItem {
param: CallbackDataParams;
dataItem: TimeSeriesChartDataItem;
}
interface TooltipParams {
items: TooltipItem[];
comparisonItems: TooltipItem[];
}
export const createTooltipValueFormatFunction =
(tooltipValueFormatter: string | TimeSeriesChartTooltipValueFormatFunction): TimeSeriesChartTooltipValueFormatFunction => {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
if (isFunction(tooltipValueFormatter)) {
tooltipValueFormatFunction = tooltipValueFormatter as TimeSeriesChartTooltipValueFormatFunction;
} else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) {
try {
tooltipValueFormatFunction =
new Function('value', 'latestData', tooltipValueFormatter) as TimeSeriesChartTooltipValueFormatFunction;
} catch (e) {
}
}
return tooltipValueFormatFunction;
};
export class TimeSeriesChartTooltip {
constructor(private renderer: Renderer2,
private sanitizer: DomSanitizer,
private settings: TimeSeriesChartTooltipWidgetSettings,
private tooltipDateFormat: DateFormatProcessor,
private valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction) {
}
formatted(params: CallbackDataParams[] | CallbackDataParams, focusedSeriesIndex: number,
series?: TimeSeriesChartDataItem[], interval?: Interval): HTMLElement {
if (!this.settings.showTooltip) {
return undefined;
}
const tooltipParams = TimeSeriesChartTooltip.mapTooltipParams(params, series, focusedSeriesIndex);
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null;
}
const tooltipElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(tooltipElement, 'display', 'flex');
this.renderer.setStyle(tooltipElement, 'flex-direction', 'column');
this.renderer.setStyle(tooltipElement, 'align-items', 'flex-start');
this.renderer.setStyle(tooltipElement, 'gap', '16px');
this.buildItemsTooltip(tooltipElement, tooltipParams.items, interval);
this.buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, interval);
return tooltipElement;
}
private buildItemsTooltip(tooltipElement: HTMLElement,
items: TooltipItem[], interval?: Interval) {
if (items.length) {
const tooltipItemsElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(tooltipItemsElement, 'display', 'flex');
this.renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column');
this.renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start');
this.renderer.setStyle(tooltipItemsElement, 'gap', '4px');
this.renderer.appendChild(tooltipElement, tooltipItemsElement);
if (this.settings.tooltipShowDate) {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval));
}
for (const item of items) {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item));
}
}
}
private constructTooltipDateElement(param: CallbackDataParams, interval?: Interval): HTMLElement {
const dateElement: HTMLElement = this.renderer.createElement('div');
let dateText: string;
const startTs = param.value[2];
const endTs = param.value[3];
if (this.settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = this.tooltipDateFormat.update(startTs, interval);
const endDateText = this.tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = param.value[0];
dateText = this.tooltipDateFormat.update(ts, interval);
}
this.renderer.appendChild(dateElement, this.renderer.createText(dateText));
this.renderer.setStyle(dateElement, 'font-family', this.settings.tooltipDateFont.family);
this.renderer.setStyle(dateElement, 'font-size', this.settings.tooltipDateFont.size + this.settings.tooltipDateFont.sizeUnit);
this.renderer.setStyle(dateElement, 'font-style', this.settings.tooltipDateFont.style);
this.renderer.setStyle(dateElement, 'font-weight', this.settings.tooltipDateFont.weight);
this.renderer.setStyle(dateElement, 'line-height', this.settings.tooltipDateFont.lineHeight);
this.renderer.setStyle(dateElement, 'color', this.settings.tooltipDateColor);
return dateElement;
}
private constructTooltipSeriesElement(item: TooltipItem): HTMLElement {
const labelValueElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(labelValueElement, 'display', 'flex');
this.renderer.setStyle(labelValueElement, 'flex-direction', 'row');
this.renderer.setStyle(labelValueElement, 'align-items', 'center');
this.renderer.setStyle(labelValueElement, 'align-self', 'stretch');
this.renderer.setStyle(labelValueElement, 'gap', '12px');
const labelElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(labelElement, 'display', 'flex');
this.renderer.setStyle(labelElement, 'align-items', 'center');
this.renderer.setStyle(labelElement, 'gap', '8px');
this.renderer.appendChild(labelValueElement, labelElement);
const circleElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(circleElement, 'width', '8px');
this.renderer.setStyle(circleElement, 'height', '8px');
this.renderer.setStyle(circleElement, 'border-radius', '50%');
this.renderer.setStyle(circleElement, 'background', item.param.color);
this.renderer.appendChild(labelElement, circleElement);
const labelTextElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setProperty(labelTextElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, item.param.seriesName));
this.renderer.setStyle(labelTextElement, 'font-family', this.settings.tooltipLabelFont.family);
this.renderer.setStyle(labelTextElement, 'font-size', this.settings.tooltipLabelFont.size + this.settings.tooltipLabelFont.sizeUnit);
this.renderer.setStyle(labelTextElement, 'font-style', this.settings.tooltipLabelFont.style);
this.renderer.setStyle(labelTextElement, 'font-weight', this.settings.tooltipLabelFont.weight);
this.renderer.setStyle(labelTextElement, 'line-height', this.settings.tooltipLabelFont.lineHeight);
this.renderer.setStyle(labelTextElement, 'color', this.settings.tooltipLabelColor);
this.renderer.appendChild(labelElement, labelTextElement);
const valueElement: HTMLElement = this.renderer.createElement('div');
let formatFunction = this.valueFormatFunction;
let latestData: FormattedData;
let units = '';
let decimals = 0;
if (item.dataItem) {
if (item.dataItem.tooltipValueFormatFunction) {
formatFunction = item.dataItem.tooltipValueFormatFunction;
}
latestData = item.dataItem.latestData;
units = item.dataItem.units;
decimals = item.dataItem.decimals;
}
if (!latestData) {
latestData = {} as FormattedData;
}
const value = formatFunction(item.param.value[1], latestData, units, decimals);
this.renderer.setProperty(valueElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, value));
this.renderer.setStyle(valueElement, 'flex', '1');
this.renderer.setStyle(valueElement, 'text-align', 'end');
this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family);
this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit);
this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style);
this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight);
this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight);
this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor);
this.renderer.appendChild(labelValueElement, valueElement);
return labelValueElement;
}
private static mapTooltipParams(params: CallbackDataParams[] | CallbackDataParams,
series?: TimeSeriesChartDataItem[],
focusedSeriesIndex?: number): TooltipParams {
const result: TooltipParams = {
items: [],
comparisonItems: []
};
if (!params || Array.isArray(params) && !params[0]) {
return result;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return result;
}
let seriesParams: CallbackDataParams = null;
if (Array.isArray(params) && focusedSeriesIndex > -1) {
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex);
} else if (!Array.isArray(params)) {
seriesParams = params;
}
if (seriesParams) {
TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series);
} else if (Array.isArray(params)) {
for (seriesParams of params) {
TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series);
}
}
return result;
}
private static appendTooltipItem(tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: TimeSeriesChartDataItem[]) {
const dataItem = series?.find(s => s.id === seriesParams.seriesId);
const tooltipItem: TooltipItem = {
param: seriesParams,
dataItem
};
if (dataItem?.comparisonItem) {
tooltipParams.comparisonItems.push(tooltipItem);
} else {
tooltipParams.items.push(tooltipItem);
}
};
}

View File

@ -74,20 +74,20 @@
<th>
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container>
</th>
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].min }}
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'">
</td>
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].max }}
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'">
</td>
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].avg }}
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].avg | safe: 'html'">
</td>
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].total }}
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].total | safe: 'html'">
</td>
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].latest }}
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].latest | safe: 'html'">
</td>
</tr>
</tbody>
@ -106,15 +106,15 @@
(mouseleave)="onLegendKeyLeave(legendKey)"
(click)="toggleLegendKey(legendKey)">
<div class="tb-time-series-chart-legend-item-label-circle" [style]="{background: !legendKey.dataKey.hidden ? legendKey.dataKey.color : null}"></div>
<div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle">{{ legendKey.dataKey.label }}</div>
<div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle" [innerHTML]="legendKey.dataKey.label | safe: 'html'"></div>
</div>
</div>
</ng-template>
<ng-template #legendDataRow let-label="label" let-type="type">
<tr>
<th class="tb-time-series-chart-legend-type-label" [style]="legendColumnTitleStyle">{{ label | translate }}</th>
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex][type] }}
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex][type] | safe: 'html'">
</td>
</tr>
</ng-template>

View File

@ -23,15 +23,12 @@ import {
autoDateFormat,
AutoDateFormatSettings,
ComponentStyle,
DateFormatProcessor,
DateFormatSettings,
Font,
tsToFormatTimeUnit,
ValueSourceConfig,
ValueSourceType
} from '@shared/models/widget-settings.models';
import {
CallbackDataParams,
TimeAxisBandWidthCalculator,
VisualMapComponentOption,
XAXisOption,
@ -75,12 +72,10 @@ import { BuiltinTextPosition } from 'zrender/src/core/types';
import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel';
import {
calculateAggIntervalWithWidgetTimeWindow,
Interval,
IntervalMath,
WidgetTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { Renderer2 } from '@angular/core';
import {
chartAnimationDefaultSettings,
ChartAnimationSettings,
@ -98,6 +93,11 @@ import {
prepareChartThemeColor
} from '@home/components/widget/lib/chart/chart.models';
import { BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries';
import {
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction,
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
type TimeSeriesChartDataEntry = [number, any, number, number];
@ -127,9 +127,6 @@ const toTimeSeriesChartDataEntry = (entry: DataEntry, valueConverter?: (value: a
return item;
};
export type TimeSeriesChartTooltipValueFormatFunction =
(value: any, latestData: FormattedData, units?: string, decimals?: number) => string;
export interface TimeSeriesChartDataItem {
id: string;
datasource: Datasource;
@ -216,245 +213,6 @@ export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption,
timeAxisOption.max = (typeof max !== 'undefined' && Math.abs(max - defaultMax) < 1000) ? max : defaultMax;
};
export enum TimeSeriesChartTooltipTrigger {
point = 'point',
axis = 'axis'
}
export const tooltipTriggerTranslationMap = new Map<TimeSeriesChartTooltipTrigger, string>(
[
[ TimeSeriesChartTooltipTrigger.point, 'tooltip.trigger-point' ],
[ TimeSeriesChartTooltipTrigger.axis, 'tooltip.trigger-axis' ]
]
);
export interface TimeSeriesChartTooltipWidgetSettings {
showTooltip: boolean;
tooltipTrigger?: TimeSeriesChartTooltipTrigger;
tooltipShowFocusedSeries?: boolean;
tooltipLabelFont: Font;
tooltipLabelColor: string;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipValueFormatter?: string | TimeSeriesChartTooltipValueFormatFunction;
tooltipShowDate: boolean;
tooltipDateInterval?: boolean;
tooltipDateFormat: DateFormatSettings;
tooltipDateFont: Font;
tooltipDateColor: string;
tooltipBackgroundColor: string;
tooltipBackgroundBlur: number;
}
export const createTooltipValueFormatFunction =
(tooltipValueFormatter: string | TimeSeriesChartTooltipValueFormatFunction): TimeSeriesChartTooltipValueFormatFunction => {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
if (isFunction(tooltipValueFormatter)) {
tooltipValueFormatFunction = tooltipValueFormatter as TimeSeriesChartTooltipValueFormatFunction;
} else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) {
try {
tooltipValueFormatFunction =
new Function('value', 'latestData', tooltipValueFormatter) as TimeSeriesChartTooltipValueFormatFunction;
} catch (e) {}
}
return tooltipValueFormatFunction;
};
export const timeSeriesChartTooltipFormatter = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: TimeSeriesChartTooltipWidgetSettings,
params: CallbackDataParams[] | CallbackDataParams,
valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction,
focusedSeriesIndex: number,
series?: TimeSeriesChartDataItem[],
interval?: Interval): null | HTMLElement => {
const tooltipParams = mapTooltipParams(params, series, focusedSeriesIndex);
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null;
}
const tooltipElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipElement, 'display', 'flex');
renderer.setStyle(tooltipElement, 'flex-direction', 'column');
renderer.setStyle(tooltipElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipElement, 'gap', '16px');
buildItemsTooltip(tooltipElement, tooltipParams.items, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
return tooltipElement;
};
interface TooltipItem {
param: CallbackDataParams;
dataItem: TimeSeriesChartDataItem;
}
interface TooltipParams {
items: TooltipItem[];
comparisonItems: TooltipItem[];
}
const buildItemsTooltip = (tooltipElement: HTMLElement,
items: TooltipItem[],
renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: TimeSeriesChartTooltipWidgetSettings,
valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction,
interval?: Interval) => {
if (items.length) {
const tooltipItemsElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipItemsElement, 'display', 'flex');
renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column');
renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipItemsElement, 'gap', '4px');
renderer.appendChild(tooltipElement, tooltipItemsElement);
if (settings.tooltipShowDate) {
renderer.appendChild(tooltipItemsElement,
constructTooltipDateElement(renderer, tooltipDateFormat, settings, items[0].param, interval));
}
for (const item of items) {
renderer.appendChild(tooltipItemsElement,
constructTooltipSeriesElement(renderer, settings, item, valueFormatFunction));
}
}
};
const mapTooltipParams = (params: CallbackDataParams[] | CallbackDataParams,
series?: TimeSeriesChartDataItem[],
focusedSeriesIndex?: number): TooltipParams => {
const result: TooltipParams = {
items: [],
comparisonItems: []
};
if (!params || Array.isArray(params) && !params[0]) {
return result;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return result;
}
let seriesParams: CallbackDataParams = null;
if (Array.isArray(params) && focusedSeriesIndex > -1) {
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex);
} else if (!Array.isArray(params)) {
seriesParams = params;
}
if (seriesParams) {
appendTooltipItem(result, seriesParams, series);
} else if (Array.isArray(params)) {
for (seriesParams of params) {
appendTooltipItem(result, seriesParams, series);
}
}
return result;
};
const appendTooltipItem = (tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: TimeSeriesChartDataItem[]) => {
const dataItem = series?.find(s => s.id === seriesParams.seriesId);
const tooltipItem: TooltipItem = {
param: seriesParams,
dataItem
};
if (dataItem?.comparisonItem) {
tooltipParams.comparisonItems.push(tooltipItem);
} else {
tooltipParams.items.push(tooltipItem);
}
};
const constructTooltipDateElement = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: TimeSeriesChartTooltipWidgetSettings,
param: CallbackDataParams,
interval?: Interval): HTMLElement => {
const dateElement: HTMLElement = renderer.createElement('div');
let dateText: string;
const startTs = param.value[2];
const endTs = param.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = param.value[0];
dateText = tooltipDateFormat.update(ts, interval);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style);
renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight);
renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight);
renderer.setStyle(dateElement, 'color', settings.tooltipDateColor);
return dateElement;
};
const constructTooltipSeriesElement = (renderer: Renderer2,
settings: TimeSeriesChartTooltipWidgetSettings,
item: TooltipItem,
valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction): HTMLElement => {
const labelValueElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelValueElement, 'display', 'flex');
renderer.setStyle(labelValueElement, 'flex-direction', 'row');
renderer.setStyle(labelValueElement, 'align-items', 'center');
renderer.setStyle(labelValueElement, 'align-self', 'stretch');
renderer.setStyle(labelValueElement, 'gap', '12px');
const labelElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelElement, 'display', 'flex');
renderer.setStyle(labelElement, 'align-items', 'center');
renderer.setStyle(labelElement, 'gap', '8px');
renderer.appendChild(labelValueElement, labelElement);
const circleElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(circleElement, 'width', '8px');
renderer.setStyle(circleElement, 'height', '8px');
renderer.setStyle(circleElement, 'border-radius', '50%');
renderer.setStyle(circleElement, 'background', item.param.color);
renderer.appendChild(labelElement, circleElement);
const labelTextElement: HTMLElement = renderer.createElement('div');
renderer.appendChild(labelTextElement, renderer.createText(item.param.seriesName));
renderer.setStyle(labelTextElement, 'font-family', settings.tooltipLabelFont.family);
renderer.setStyle(labelTextElement, 'font-size', settings.tooltipLabelFont.size + settings.tooltipLabelFont.sizeUnit);
renderer.setStyle(labelTextElement, 'font-style', settings.tooltipLabelFont.style);
renderer.setStyle(labelTextElement, 'font-weight', settings.tooltipLabelFont.weight);
renderer.setStyle(labelTextElement, 'line-height', settings.tooltipLabelFont.lineHeight);
renderer.setStyle(labelTextElement, 'color', settings.tooltipLabelColor);
renderer.appendChild(labelElement, labelTextElement);
const valueElement: HTMLElement = renderer.createElement('div');
let formatFunction = valueFormatFunction;
let latestData: FormattedData;
let units = '';
let decimals = 0;
if (item.dataItem) {
if (item.dataItem.tooltipValueFormatFunction) {
formatFunction = item.dataItem.tooltipValueFormatFunction;
}
latestData = item.dataItem.latestData;
units = item.dataItem.units;
decimals = item.dataItem.decimals;
}
if (!latestData) {
latestData = {} as FormattedData;
}
const value = formatFunction(item.param.value[1], latestData, units, decimals);
renderer.appendChild(valueElement, renderer.createText(value));
renderer.setStyle(valueElement, 'flex', '1');
renderer.setStyle(valueElement, 'text-align', 'end');
renderer.setStyle(valueElement, 'font-family', settings.tooltipValueFont.family);
renderer.setStyle(valueElement, 'font-size', settings.tooltipValueFont.size + settings.tooltipValueFont.sizeUnit);
renderer.setStyle(valueElement, 'font-style', settings.tooltipValueFont.style);
renderer.setStyle(valueElement, 'font-weight', settings.tooltipValueFont.weight);
renderer.setStyle(valueElement, 'line-height', settings.tooltipValueFont.lineHeight);
renderer.setStyle(valueElement, 'color', settings.tooltipValueColor);
renderer.appendChild(labelValueElement, valueElement);
return labelValueElement;
};
export enum TimeSeriesChartType {
default = 'default',

View File

@ -21,7 +21,6 @@ import {
createTimeSeriesVisualMapOption,
createTimeSeriesXAxis,
createTimeSeriesYAxis,
createTooltipValueFormatFunction,
defaultTimeSeriesChartYAxisSettings,
generateChartData,
LineSeriesStepType,
@ -37,9 +36,6 @@ import {
TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartThresholdItem,
timeSeriesChartTooltipFormatter,
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction,
TimeSeriesChartType,
TimeSeriesChartXAxis,
TimeSeriesChartYAxis,
@ -74,6 +70,12 @@ import { DeepPartial } from '@shared/models/common';
import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models';
import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models';
import { ChartLabelPosition, ChartShape, toAnimationOption } from '@home/components/widget/lib/chart/chart.models';
import {
createTooltipValueFormatFunction,
TimeSeriesChartTooltip,
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export class TbTimeSeriesChart {
@ -133,7 +135,7 @@ export class TbTimeSeriesChart {
private timeSeriesChartOptions: EChartsOption;
private readonly tooltipDateFormat: DateFormatProcessor;
private readonly tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
private readonly timeSeriesChartTooltip: TimeSeriesChartTooltip;
private readonly stateValueConverter: TimeSeriesChartStateValueConverter;
private yMinSubject = new BehaviorSubject(-1);
@ -162,6 +164,8 @@ export class TbTimeSeriesChart {
private renderer: Renderer2,
private autoResize = true) {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings);
@ -169,7 +173,7 @@ export class TbTimeSeriesChart {
this.stackMode = !this.comparisonEnabled && this.settings.stack;
if (this.settings.states && this.settings.states.length) {
this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states);
this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
}
const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null;
@ -183,14 +187,20 @@ export class TbTimeSeriesChart {
if (this.settings.tooltipShowDate) {
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
}
if (!this.tooltipValueFormatFunction) {
this.tooltipValueFormatFunction =
createTooltipValueFormatFunction(this.settings.tooltipValueFormatter);
if (!this.tooltipValueFormatFunction) {
this.tooltipValueFormatFunction = (value, _latestData, units, decimals) => formatValue(value, decimals, units, false);
if (!tooltipValueFormatFunction) {
tooltipValueFormatFunction = createTooltipValueFormatFunction(this.settings.tooltipValueFormatter);
if (!tooltipValueFormatFunction) {
tooltipValueFormatFunction = (value, _latestData, units, decimals) => formatValue(value, decimals, units, false);
}
}
}
this.timeSeriesChartTooltip = new TimeSeriesChartTooltip(
this.renderer,
this.ctx.sanitizer,
this.settings,
this.tooltipDateFormat,
tooltipValueFormatFunction
);
this.onResize();
if (this.autoResize) {
this.shapeResize$ = new ResizeObserver(() => {
@ -603,10 +613,12 @@ export class TbTimeSeriesChart {
type: this.noAggregation ? 'line' : 'shadow'
},
formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? timeSeriesChartTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.tooltipValueFormatFunction,
this.timeSeriesChartTooltip.formatted(
params,
this.settings.tooltipShowFocusedSeries ? getFocusedSeriesIndex(this.timeSeriesChart) : -1,
this.dataItems, this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined,
this.dataItems,
this.noAggregation ? null : this.ctx.timeWindow.interval,
),
padding: [8, 12],
backgroundColor: this.settings.tooltipBackgroundColor,
borderWidth: 0,

View File

@ -0,0 +1,79 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="unreadNotificationWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.max-notification-display' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxNotificationDisplay" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="enableFilter">
{{ 'widgets.notification.type-filter' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="enableMarkAsRead">
{{ 'widgets.notification.button-mark-read' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="enableViewAll">
{{ 'widgets.notification.button-view-all' | translate }}
</mat-slide-toggle>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-row no-padding no-border">
<mat-slide-toggle class="mat-slide" formControlName="showCounter">
{{ 'widgets.notification.counter' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.counter-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="counterValueFont"
clearButton
[previewText]="unreadNotificationWidgetSettingsForm.get('maxNotificationDisplay').value.toString()"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="counterValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.counter-color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="counterColor">
</tb-color-input>
</div>
</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">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</ng-container>

View File

@ -0,0 +1,81 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { unreadNotificationDefaultSettings } from '@home/components/widget/lib/cards/unread-notification-widget.models';
@Component({
selector: 'tb-unread-notification-widget-settings',
templateUrl: './unread-notification-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class UnreadNotificationWidgetSettingsComponent extends WidgetSettingsComponent {
unreadNotificationWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.unreadNotificationWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...unreadNotificationDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.unreadNotificationWidgetSettingsForm = this.fb.group({
maxNotificationDisplay: [settings?.maxNotificationDisplay, [Validators.required, Validators.min(1)]],
showCounter: [settings?.showCounter, []],
counterValueFont: [settings?.counterValueFont, []],
counterValueColor: [settings?.counterValueColor, []],
counterColor: [settings?.counterColor, []],
enableViewAll: [settings?.enableViewAll, []],
enableFilter: [settings?.enableFilter, []],
enableMarkAsRead: [settings?.enableMarkAsRead, []],
background: [settings?.background, []],
padding: [settings.padding, []]
});
}
protected validatorTriggers(): string[] {
return ['showCounter'];
}
protected updateValidators(emitEvent: boolean) {
const showCounter: boolean = this.unreadNotificationWidgetSettingsForm.get('showCounter').value;
if (showCounter) {
this.unreadNotificationWidgetSettingsForm.get('counterValueFont').enable({emitEvent});
this.unreadNotificationWidgetSettingsForm.get('counterValueColor').enable({emitEvent});
this.unreadNotificationWidgetSettingsForm.get('counterColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetSettingsForm.get('counterValueFont').disable({emitEvent});
this.unreadNotificationWidgetSettingsForm.get('counterValueColor').disable({emitEvent});
this.unreadNotificationWidgetSettingsForm.get('counterColor').disable({emitEvent});
}
}
}

View File

@ -33,7 +33,6 @@ import {
TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import {
TimeSeriesChartTooltipTrigger,
TimeSeriesChartKeySettings,
TimeSeriesChartType,
TimeSeriesChartYAxes,
@ -41,6 +40,7 @@ import {
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetService } from '@core/http/widget.service';
import { TimeSeriesChartTooltipTrigger } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
@Component({
selector: 'tb-time-series-chart-widget-settings',

View File

@ -141,8 +141,8 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
writeValue(value: ColorGradientSettings): void {
if (isDefinedAndNotNull(value)) {
this.gradientFormGroup.get('advancedMode').patchValue(value.advancedMode, {emitEvent: false});
this.gradientFormGroup.get('minValue').patchValue(isFinite(value.minValue) ? value.minValue : this.minValue, {emitEvent: false});
this.gradientFormGroup.get('maxValue').patchValue(isFinite(value.maxValue) ? value.maxValue : this.maxValue, {emitEvent: false});
this.gradientFormGroup.get('minValue').patchValue(isFinite(this.minValue) ? this.minValue : value.minValue, {emitEvent: false});
this.gradientFormGroup.get('maxValue').patchValue(isFinite(this.maxValue) ? this.maxValue : value.maxValue, {emitEvent: false});
if (value?.gradient?.length) {
this.gradientFormGroup.get('gradient').get('start').patchValue(value.gradient[0], {emitEvent: false});
this.gradientFormGroup.get('gradient').get('end').patchValue(value.gradient[value.gradient.length - 1], {emitEvent: false});

View File

@ -362,6 +362,9 @@ import {
import {
LabelValueCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/label-value-card-widget-settings.component';
import {
UnreadNotificationWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/unread-notification-widget-settings.component';
@NgModule({
declarations: [
@ -490,7 +493,8 @@ import {
PolarAreaChartWidgetSettingsComponent,
RadarChartWidgetSettingsComponent,
LabelCardWidgetSettingsComponent,
LabelValueCardWidgetSettingsComponent
LabelValueCardWidgetSettingsComponent,
UnreadNotificationWidgetSettingsComponent,
],
imports: [
CommonModule,
@ -624,7 +628,8 @@ import {
PolarAreaChartWidgetSettingsComponent,
RadarChartWidgetSettingsComponent,
LabelCardWidgetSettingsComponent,
LabelValueCardWidgetSettingsComponent
LabelValueCardWidgetSettingsComponent,
UnreadNotificationWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -725,5 +730,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-polar-area-chart-widget-settings': PolarAreaChartWidgetSettingsComponent,
'tb-radar-chart-widget-settings': RadarChartWidgetSettingsComponent,
'tb-label-card-widget-settings': LabelCardWidgetSettingsComponent,
'tb-label-value-card-widget-settings': LabelValueCardWidgetSettingsComponent
'tb-label-value-card-widget-settings': LabelValueCardWidgetSettingsComponent,
'tb-unread-notification-widget-settings': UnreadNotificationWidgetSettingsComponent
};

View File

@ -106,6 +106,12 @@ import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards
import {
RestConnectorSecurityComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component';
import {
UnreadNotificationWidgetComponent
} from '@home/components/widget/lib/cards/unread-notification-widget.component';
import {
NotificationTypeFilterPanelComponent
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api';
import {
BrokerConfigControlComponent,
@ -184,8 +190,9 @@ import {
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
],
LabelValueCardWidgetComponent,
UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent],
imports: [
CommonModule,
SharedModule,
@ -264,7 +271,9 @@ import {
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
LabelValueCardWidgetComponent,
UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}

View File

@ -95,7 +95,7 @@
</tb-widget>
</div>
</div>
<ng-template #widgetTitlePanel>
<ng-template #widgetTitlePanel let-titleSuffixTemplate="titleSuffixTemplate">
<div *ngIf="widget.showWidgetTitlePanel"
class="tb-widget-title">
<div *ngIf="widget.showTitle"
@ -107,6 +107,7 @@
<div class="mat-subtitle-1 title" [style]="widget.titleStyle">
{{widget.title$ | async}}
</div>
<ng-container *ngTemplateOutlet="titleSuffixTemplate"></ng-container>
</div>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"

View File

@ -16,7 +16,7 @@
-->
<section fxLayout="row" fxLayoutAlign="space-between start" class="notification"
[ngStyle]="{borderColor: notificationColor()}">
[ngStyle]="{borderColor: notificationColor(), backgroundColor: notificationBackgroundColor()}">
<div *ngIf="showIcon; else defaultIcon">
<tb-icon class="icon" [ngStyle]="{color: notification.additionalConfig.icon.color}">
{{ notification.additionalConfig.icon.icon }}

View File

@ -145,6 +145,13 @@ export class NotificationComponent implements OnInit {
return 'transparent';
}
notificationBackgroundColor(): string {
if (this.notification.type === NotificationType.ALARM && !this.notification.info.cleared) {
return '#fff';
}
return 'transparent';
}
notificationIconColor(): object {
if (this.notification.type === NotificationType.ALARM) {
return {color: AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)};

View File

@ -133,6 +133,7 @@ export const HelpLinks = {
ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table`,
ruleNodeRuleChain: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#rule-chain-node`,
ruleNodeOutputNode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#output-node`,
ruleNodeAwsLambda: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-lambda-node`,
ruleNodeAwsSns: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node`,
ruleNodeAwsSqs: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node`,
ruleNodeKafka: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#kafka-node`,

View File

@ -493,6 +493,7 @@ const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes',
'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries',
'org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode': 'ruleNodeSaveToCustomTable',
'org.thingsboard.rule.engine.aws.lambda.TbLambdaNode': 'ruleNodeAwsLambda',
'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns',
'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs',
'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka',

View File

@ -38,7 +38,7 @@ import { entityFields } from '@shared/models/entity.models';
import { isDefinedAndNotNull, isUndefined } from '@core/utils';
import { CmdWrapper, WsService, WsSubscriber } from '@shared/models/websocket/websocket.models';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { Notification } from '@shared/models/notification.models';
import { Notification, NotificationType } from '@shared/models/notification.models';
import { WebsocketService } from '@core/ws/websocket.service';
export const NOT_SUPPORTED = 'Not supported!';
@ -304,11 +304,14 @@ export class UnreadCountSubCmd implements WebsocketCmd {
export class UnreadSubCmd implements WebsocketCmd {
limit: number;
types: Array<NotificationType>;
cmdId: number;
type = WsCmdType.NOTIFICATIONS;
constructor(limit = 10) {
constructor(limit = 10,
types: Array<NotificationType> = []) {
this.limit = limit;
this.types = types;
}
}
@ -911,6 +914,8 @@ export class NotificationSubscriber extends WsSubscriber {
public messageLimit = 10;
public notificationType = [];
public notificationCount$ = this.notificationCountSubject.asObservable().pipe(map(msg => msg.totalUnreadCount));
public notifications$ = this.notificationsSubject.asObservable().pipe(map(msg => msg.notifications ));
@ -923,8 +928,8 @@ export class NotificationSubscriber extends WsSubscriber {
}
public static createNotificationsSubscription(websocketService: WebsocketService<WsSubscriber>,
zone: NgZone, limit = 10): NotificationSubscriber {
const subscriptionCommand = new UnreadSubCmd(limit);
zone: NgZone, limit = 10, types: Array<NotificationType> = []): NotificationSubscriber {
const subscriptionCommand = new UnreadSubCmd(limit, types);
const subscriber = new NotificationSubscriber(websocketService, zone);
subscriber.messageLimit = limit;
subscriber.subscriptionCommands.push(subscriptionCommand);

View File

@ -7043,6 +7043,22 @@
"bar-background": "Bar background",
"progress-bar-card-style": "Progress bar card style"
},
"notification": {
"max-notification-display": "Maximum notifications to display",
"counter": "Counter",
"icon": "Icon",
"counter-value": "Value",
"counter-color": "Color",
"notification-button": "Notification buttons",
"button-view-all": "View all",
"button-filter": "Filter",
"type-filter": "Type filter",
"button-mark-read": "Mark as read",
"notification-types": "Notification types",
"notification-type": "Notification type",
"search-type": "Search type",
"any-type": "Any type"
},
"alarm-count": {
"alarm-count-card-style": "Alarm count card style"
},

View File

@ -1 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="234" height="245" fill="none"><rect width="100%" height="100%"/><mask id="a" fill="#fff"><path d="M144.998 161.896a18.006 18.006 0 0 1-.561 13.766 17.988 17.988 0 0 1-10.13 9.336 18.001 18.001 0 0 1-23.102-10.691l16.897-6.205 16.896-6.206Z"/></mask><g class="currentLayer"><path fill="#fff" d="M0 0h234v245H0z"/><path d="M3.186 131.261a100 100 0 0 1 23.92-91.658l6.805-7.324C71.513-8.188 134.803-10.502 175.26 27.111l26.547 24.682c44.827 41.677 42.031 113.484-5.902 151.548l-24.43 19.4c-57.171 45.4-142.145 16.223-159.366-54.722l-8.922-36.758Z" fill="#F3F7FF"/><circle opacity=".4" cx="195" cy="67" r="6" fill="#6794C7"/><circle opacity=".4" cx="121.5" cy="15.5" r="10.5" fill="#6794C7"/><circle opacity=".4" cx="41" cy="73" r="9" fill="#6794C7"/><circle opacity=".4" cx="116.5" cy="47.5" r="2.5" fill="#6794C7"/><circle opacity=".4" cx="196.5" cy="179.5" r="3.5" fill="#6794C7"/><circle opacity=".4" cx="102.5" cy="185.5" r="3.5" fill="#6794C7"/><circle opacity=".4" cx="28" cy="150" r="6" fill="#6794C7"/><path d="M144.998 161.896a18.006 18.006 0 0 1-.561 13.766 17.988 17.988 0 0 1-10.13 9.336 18.001 18.001 0 0 1-23.102-10.691l16.897-6.205 16.896-6.206Z" fill="#fff" stroke="#6794C7" stroke-width="6" mask="url(#a)"/><ellipse opacity=".4" cx="113" cy="202" rx="70" ry="5" fill="#6794C7"/><path d="m76.238 66.355.9-.666-.383-1.052-.705-1.938a12.722 12.722 0 0 1 23.91-8.703l.705 1.938.383 1.053 1.118-.069c21.32-1.302 40.916 11.6 48.23 31.693l12.326 33.866a17.494 17.494 0 0 0 9.049 9.875l11.659 5.437 1.707 4.689L67.675 185.23l-1.706-4.688 5.436-11.66a17.5 17.5 0 0 0 .585-13.381l-12.327-33.866c-7.313-20.093-.595-42.573 16.575-55.28Z" fill="#fff" stroke="#6794C7" stroke-width="3"/></g></svg>
<svg id="CHECK_ICON" width="149" height="156" viewBox="0 0 149 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.51475 83.5197C-2.49495 62.8814 3.18781 41.1064 17.644 25.5483L21.948 20.9161C45.73 -4.67877 85.76 -6.14243 111.348 17.6473L128.138 33.2579C156.49 59.6177 154.721 105.034 124.405 129.109L108.954 141.379C72.7942 170.094 19.05 151.639 8.15805 106.768L2.51475 83.5197ZM118 90L43.5 117.5L42 114C43.5 111 46.6 104.1 47 100.5C47.1929 98.7641 45.8232 95.6888 44.0104 91.6186C41.124 85.1379 37.1142 76.1349 36.5 66C35.7 52.8 44.8333 43.8334 49.5 41C48.5 38.3334 47.9 32.3 53.5 29.5C59.1 26.7 63.1667 33 64.5 36.5C65.8714 36.7613 67.4156 36.8817 69.0836 37.0119C75.9294 37.546 84.8612 38.2429 92.5 49.5C96.0351 54.7096 98.5627 62.5506 100.707 69.2021C102.446 74.5966 103.933 79.2087 105.5 81C108.3 84.2 114 86.3334 116.5 87L118 90ZM72.4999 111.5L91.4999 104C92.6665 106.833 93.1999 113.3 85.9999 116.5C78.7999 119.7 73.9999 114.5 72.4999 111.5Z" fill="currentColor" fill-opacity="0.06"/>
<circle opacity="0.4" cx="123.833" cy="42.876" r="3.79487" fill="currentColor" fill-opacity="0.24"/>
<circle opacity="0.4" cx="77.3461" cy="10.3034" r="6.64103" fill="currentColor" fill-opacity="0.24"/>
<circle opacity="0.4" cx="26.4316" cy="46.6709" r="5.69231" fill="currentColor" fill-opacity="0.24"/>
<circle opacity="0.4" cx="74.1837" cy="30.5427" r="1.5812" fill="currentColor" fill-opacity="0.24"/>
<circle opacity="0.4" cx="124.782" cy="114.03" r="2.21368" fill="currentColor" fill-opacity="0.24"/>
<circle opacity="0.4" cx="65.329" cy="117.825" r="2.21368" fill="currentColor" fill-opacity="0.24"/>
<circle opacity="0.4" cx="18.2094" cy="95.3718" r="3.79487" fill="currentColor" fill-opacity="0.24"/>
<path d="M92.2083 102.896C92.7237 104.299 92.9577 105.791 92.8968 107.284C92.8359 108.778 92.4814 110.245 91.8535 111.602C91.2256 112.959 90.3366 114.179 89.2373 115.192C88.138 116.205 86.8498 116.992 85.4464 117.508C84.043 118.023 82.5519 118.257 81.058 118.196C79.5642 118.135 78.097 117.781 76.7402 117.153C75.3834 116.525 74.1636 115.636 73.1504 114.536C72.1371 113.437 71.3504 112.149 70.8349 110.746L81.5216 106.821L92.2083 102.896Z" stroke="currentColor" stroke-width="1.89744"/>
<ellipse opacity="0.16" cx="71.9699" cy="128.261" rx="44.2735" ry="3.16239" fill="currentColor"/>
<path d="M48.7194 42.2145L49.2902 41.7946L49.0463 41.1286L48.6002 39.9105C47.8712 37.9201 47.9674 35.7227 48.8689 33.8013C49.7705 31.8798 51.4039 30.3917 53.4105 29.6659C55.4171 28.9401 57.6307 29.0365 59.5642 29.9326C61.4976 30.8286 62.9925 32.4505 63.7214 34.4408L64.1675 35.6589L64.4114 36.3249L65.1202 36.2819C78.6066 35.4632 90.9975 43.5735 95.6213 56.1988L103.405 77.4513C104.417 80.2163 106.492 82.4682 109.169 83.7088L116.516 87.1138L117.592 90.0529L43.3017 116.925L42.2252 113.986L45.6514 106.684C46.8997 104.024 47.0321 100.974 46.0195 98.2087L38.2362 76.9563C33.6124 64.3309 37.8587 50.2026 48.7194 42.2145Z" stroke="currentColor" stroke-width="1.89744"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB