diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 84f8cd9785..81cb2fdbb1 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -23,6 +23,7 @@
"cards.html_card",
"cards.html_value_card",
"cards.markdown_card",
- "cards.simple_card"
+ "cards.simple_card",
+ "unread_notifications"
]
}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/unread_notifications.json b/application/src/main/data/json/system/widget_types/unread_notifications.json
new file mode 100644
index 0000000000..7e3e6b0477
--- /dev/null
+++ b/application/src/main/data/json/system/widget_types/unread_notifications.json
@@ -0,0 +1,23 @@
+{
+ "fqn": "unread_notifications",
+ "name": "Unread notifications",
+ "deprecated": false,
+ "image": "tb-image:dW5yZWFkX25vdGlmaWNhdGlvbl9zeXN0ZW1fd2lkZ2V0X2ltYWdlLnBuZw==:IlVucmVhZCBub3RpZmljYXRpb24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/png;base64,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",
+ "description": null,
+ "descriptor": {
+ "type": "static",
+ "sizeX": 5.5,
+ "sizeY": 5,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.unreadNotificationWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '400px',\n previewHeight: '300px',\n embedTitlePanel: true\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "",
+ "dataKeySettingsSchema": "",
+ "settingsDirective": "tb-unread-notification-widget-settings",
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-unread-notification-basic-config",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
+ },
+ "tags": null
+}
diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html
index 851eff2375..d96739a5cf 100644
--- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html
+++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html
@@ -44,7 +44,11 @@
-
+
+
+
notification.no-notifications-yet
diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.scss b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.scss
new file mode 100644
index 0000000000..577c3ccb72
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.scss
@@ -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;
+}
diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts
index 6020d6586a..32fcf2dc4a 100644
--- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts
+++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts
@@ -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 {
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
index ac8cb25d60..06af5402ca 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
@@ -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
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts
new file mode 100644
index 0000000000..78fc5ce791
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts
@@ -0,0 +1,177 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component } 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,
+ 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');
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html
new file mode 100644
index 0000000000..00ec196552
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html
@@ -0,0 +1,77 @@
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.scss
new file mode 100644
index 0000000000..77a1e610d3
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.scss
@@ -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%;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts
new file mode 100644
index 0000000000..ba52310c4b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts
@@ -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('NotificationTypeFilterPanelData');
+
+export interface NotificationTypeFilterPanelData {
+ notificationTypes: Array;
+ notificationTypesUpdated: (notificationTypes: Array) => 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>;
+ selectedNotificationTypes: Array = [];
+ notificationTypesTranslateMap = NotificationTemplateTypeTranslateMap;
+
+ separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
+
+ private notificationType = NotificationType;
+ private notificationTypes = Object.keys(NotificationType) as Array;
+
+ private dirty = false;
+
+ @ViewChild('notificationTypeInput') notificationTypeInput: ElementRef;
+
+ 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;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html
new file mode 100644
index 0000000000..3537c8bb1d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ {{ count$ | async }}
+
+
+
+
+
+
+
+
+
+
+
+
+ notification.no-notifications-yet
+
+
+
+
+
notification.loading-notifications
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss
new file mode 100644
index 0000000000..5fb3ad8950
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "../../../../../../../scss/constants";
+
+.tb-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;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts
new file mode 100644
index 0000000000..fcad768e66
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts
@@ -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;
+
+ showCounter = true;
+ counterValueStyle: ComponentStyle;
+ counterBackground: string;
+
+ notifications: Notification[];
+ loadNotification = false;
+
+ backgroundStyle$: Observable;
+ overlayStyle: ComponentStyle = {};
+ padding: string;
+
+ private counterValue: BehaviorSubject = 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 = [];
+
+ 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,
+ 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) => {
+ 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();
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts
new file mode 100644
index 0000000000..008c1d9a9d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts
@@ -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'
+};
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html
new file mode 100644
index 0000000000..5f452f3508
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts
new file mode 100644
index 0000000000..1bfe38f73c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts
@@ -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,
+ 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});
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
index e4df67e2f8..778e84a614 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
@@ -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
-
+
+ [ngStyle]="{borderColor: notificationColor(), backgroundColor: notificationBackgroundColor()}">
{{ notification.additionalConfig.icon.icon }}
diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.ts b/ui-ngx/src/app/shared/components/notification/notification.component.ts
index 0d784a4138..43935bbea4 100644
--- a/ui-ngx/src/app/shared/components/notification/notification.component.ts
+++ b/ui-ngx/src/app/shared/components/notification/notification.component.ts
@@ -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)};
diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
index e1c6af620b..8cf07e1128 100644
--- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
+++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
@@ -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;
cmdId: number;
type = WsCmdType.NOTIFICATIONS;
- constructor(limit = 10) {
+ constructor(limit = 10,
+ types: Array = []) {
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,
- zone: NgZone, limit = 10): NotificationSubscriber {
- const subscriptionCommand = new UnreadSubCmd(limit);
+ zone: NgZone, limit = 10, types: Array = []): NotificationSubscriber {
+ const subscriptionCommand = new UnreadSubCmd(limit, types);
const subscriber = new NotificationSubscriber(websocketService, zone);
subscriber.messageLimit = limit;
subscriber.subscriptionCommands.push(subscriptionCommand);
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index b1b17ca3aa..b161e1e3bc 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -7001,6 +7001,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"
},
diff --git a/ui-ngx/src/assets/notification-bell.svg b/ui-ngx/src/assets/notification-bell.svg
index f5bb251293..25b9cc3086 100644
--- a/ui-ngx/src/assets/notification-bell.svg
+++ b/ui-ngx/src/assets/notification-bell.svg
@@ -1 +1,13 @@
-
+