blob: 42fd0899a549d422beadd475d308a296c716cd7e [file]
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { finalize } from 'rxjs/operators';
import { Mute } from '../../../pojo/Mute';
import { SingleAlert } from '../../../pojo/SingleAlert';
import { AlertSoundService } from '../../../service/alert-sound.service';
import { AlertService } from '../../../service/alert.service';
import { GeneralConfigService } from '../../../service/general-config.service';
@Component({
selector: 'header-notify',
template: `
<div style="display: flex; align-items: center;">
<ng-template #badgeTpl>
<nz-badge [nzCount]="count" ngClass="alain-default__nav-item" [nzStyle]="{ 'box-shadow': 'none' }">
<i nz-icon nzType="bell" ngClass="alain-default__nav-item-icon"></i>
</nz-badge>
</ng-template>
@if (data!.length <= 0) {
<ng-template [ngTemplateOutlet]="badgeTpl" />
} @else {
<div
nz-dropdown
(nzVisibleChange)="onPopoverVisibleChange($event)"
[(nzVisible)]="popoverVisible"
nzTrigger="click"
nzPlacement="bottomRight"
nzOverlayClassName="header-dropdown notice-icon"
[nzDropdownMenu]="noticeMenu"
>
<ng-template [ngTemplateOutlet]="badgeTpl" />
</div>
}
<nz-badge ngClass="alain-default__nav-item" [nzStyle]="{ 'box-shadow': 'none' }">
<i
nz-icon
[nzType]="mute.mute ? 'muted' : 'sound'"
ngClass="alain-default__nav-item-icon"
(click)="toggleMute($event)"
nz-tooltip
[nzTooltipTitle]="(mute.mute ? 'common.unmute' : 'common.mute') | i18n"
></i>
</nz-badge>
</div>
<nz-dropdown-menu #noticeMenu="nzDropdownMenu">
@if (data[0].title) {
<div class="ant-modal-title" style="line-height: 44px; text-align: center;">{{ data[0].title }}</div>
}
<nz-spin [nzSpinning]="loading" [nzDelay]="0">
@if (data[0].list && data[0].list.length > 0) {
<ng-template [ngTemplateOutlet]="listTpl" />
} @else {
<div class="notice-icon__notfound">
@if (data[0].emptyImage) {
<img class="notice-icon__notfound-img" [attr.src]="data[0].emptyImage" alt="not found" />
}
<p>
<ng-container *nzStringTemplateOutlet="data[0].emptyText">
{{ data[0].emptyText }}
</ng-container>
</p>
</div>
}
</nz-spin>
<div style="display: flex; align-items: center; border-top: 1px solid #f0f0f0;">
<div class="notice-icon__clear" style="flex: 1; border-top: none;" (click)="gotoAlertCenter()">{{ data[0].enterText }}</div>
</div>
</nz-dropdown-menu>
<ng-template #listTpl>
<nz-list [nzDataSource]="data[0].list" [nzRenderItem]="item">
<ng-template #item let-item>
<nz-list-item [class.notice-icon__item-read]="item.read">
<nz-list-item-meta [nzTitle]="nzTitle" [nzDescription]="nzDescription" [nzAvatar]="item.avatar">
<ng-template #nzTitle>
<ng-container *nzStringTemplateOutlet="item.title; context: { $implicit: item }">
<a (click)="gotoDetail(item.id)">{{ item.title }}</a>
</ng-container>
@if (item.extra) {
<div class="notice-icon__item-extra">
<nz-tag [nzColor]="item.color">{{ item.extra }}</nz-tag>
</div>
}
</ng-template>
<ng-template #nzDescription>
@if (item.description) {
<div class="notice-icon__item-desc">
<ng-container *nzStringTemplateOutlet="item.description; context: { $implicit: item }">
{{ item.description }}
</ng-container>
</div>
} @if (item.datetime) {
<div class="notice-icon__item-time">{{ item.datetime }}</div>
}
</ng-template>
</nz-list-item-meta>
</nz-list-item>
</ng-template>
</nz-list>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderNotifyComponent implements OnInit, OnDestroy {
data: any[] = [
{
title: this.i18nSvc.fanyi('dashboard.alerts.title-no'),
list: [],
emptyText: this.i18nSvc.fanyi('dashboard.alerts.no'),
emptyImage: '/assets/img/empty-full.svg',
enterText: this.i18nSvc.fanyi('dashboard.alerts.enter'),
clearText: this.i18nSvc.fanyi('alert.center.clear')
}
];
notifiedAlert: any[] = [];
count = 0;
loading = false;
popoverVisible = false;
refreshInterval: any;
private previousCount = 0;
// default to mute status
mute: Mute = { mute: true };
private eventSource!: EventSource;
constructor(
private router: Router,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private notifySvc: NzNotificationService,
private configSvc: GeneralConfigService,
private alertSvc: AlertService,
private modal: NzModalService,
private cdr: ChangeDetectorRef,
private alertSound: AlertSoundService
) {}
ngOnInit(): void {
let muteInit$ = this.configSvc
.getGeneralConfig('mute')
.pipe(
finalize(() => {
muteInit$.unsubscribe();
})
)
.subscribe(
message => {
if (message.code === 0) {
this.mute = message.data;
} else {
console.warn(message.msg);
}
},
error => {
console.error(error);
}
);
this.loadData();
this.initAlertSSEConnection();
this.initManagerSSEConnection();
}
ngOnDestroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.eventSource) {
this.eventSource.close();
}
}
onPopoverVisibleChange(visible: boolean): void {
if (visible) {
this.loadData();
}
}
updateNoticeData(notices: any[]): any[] {
const data = this.data.slice();
data.forEach(i => (i.list = []));
notices.forEach(item => {
data[0].list.push({ ...item });
});
return data;
}
loadData(): void {
if (this.loading) {
return;
}
this.loading = true;
let loadAlerts$ = this.alertSvc
.loadAlerts('firing', undefined, 0, 5)
.pipe(
finalize(() => {
loadAlerts$.unsubscribe();
this.loading = false;
})
)
.subscribe(
message => {
if (message.code === 0) {
let page = message.data;
let alerts = page.content;
if (alerts == undefined) {
return;
}
let list: any[] = [];
alerts.forEach(alert => {
let item = {
id: alert.id,
avatar: '/assets/img/notification.svg',
title: alert.content,
datetime: new Date(alert.activeAt).toLocaleString(),
color: 'blue',
status: alert.status,
type: this.i18nSvc.fanyi('dashboard.alerts.title-no')
};
list.push(item);
});
this.data = this.updateNoticeData(list);
this.count = page.totalElements;
} else {
console.warn(message.msg);
}
this.cdr.detectChanges();
},
error => {
console.error(error);
}
);
}
gotoAlertCenter(): void {
this.popoverVisible = false;
this.router.navigateByUrl(`/alert/center`);
}
gotoDetail(id: number): void {
this.popoverVisible = false;
// todo goto this alert detail pop page
// this.router.navigateByUrl(`/alarm/center/detail/${id}`);
}
toggleMute(event: MouseEvent): void {
event.stopPropagation();
const updatedMuteState = !this.mute.mute;
const updatedMuteConfig = { ...this.mute, mute: updatedMuteState };
// request notification permission
debugger;
if (!updatedMuteState) {
Notification.requestPermission().then(permission => {
if (permission !== 'granted') {
console.log('Notification permission denied.');
}
});
}
let saveConfig$ = this.configSvc
.saveGeneralConfig(updatedMuteConfig, 'mute')
.pipe(
finalize(() => {
saveConfig$.unsubscribe();
})
)
.subscribe({
next: response => {
if (response.code === 0) {
this.mute = updatedMuteConfig;
console.log('Config saved successfully', response);
} else {
console.warn('Error saving config', response.msg);
}
},
error: error => {
console.error('Error saving config', error);
},
complete: () => {
this.cdr.markForCheck();
}
});
}
private initAlertSSEConnection(): void {
const sseUrl = '/api/alert/sse/subscribe';
this.eventSource = new EventSource(sseUrl);
this.eventSource.addEventListener('ALERT_EVENT', (evt: MessageEvent) => {
let list: any[] = [];
let alert: SingleAlert = JSON.parse(evt.data);
let item = {
id: alert.id,
avatar: '/assets/img/notification.svg',
// title: `${alert.tags?.monitorName}--${this.i18nSvc.fanyi(`alert.severity.${alert.severity}`)}`,
title: alert.content,
datetime: new Date(alert.activeAt).toLocaleString(),
color: 'blue',
status: alert.status,
type: this.i18nSvc.fanyi('dashboard.alerts.title-no')
};
console.log('alert:', alert);
list.push(item);
this.data = this.updateNoticeData(list);
if (!this.mute.mute && !this.notifiedAlert.includes(alert.id)) {
this.notifiedAlert.push(alert.id);
this.alertSound.playAlertSound(this.i18nSvc.currentLang);
const notification = new Notification(this.i18nSvc.fanyi('alert.notify.title'), {
body: this.i18nSvc.fanyi('alert.notify.body'),
icon: 'assets/logo.svg'
});
notification.onclick = () => {
window.focus();
this.router.navigateByUrl(`/alert/center`);
notification.close();
};
}
this.cdr.detectChanges();
});
this.eventSource.onerror = error => {
console.error('SSE connection error:', error);
this.eventSource.close();
};
}
private initManagerSSEConnection(): void {
const sseUrl = '/api/manager/sse/subscribe';
this.eventSource = new EventSource(sseUrl);
this.eventSource.addEventListener('IMPORT_TASK_EVENT', (evt: MessageEvent) => {
let msg = JSON.parse(evt.data);
if (msg.notifyLevel === 'SUCCESS') {
this.notifySvc.success(
this.i18nSvc.fanyi('common.notice'),
this.i18nSvc.fanyi('common.notify.import-success-detail', { taskName: msg.taskName })
);
} else if (msg.notifyLevel === 'ERROR') {
this.notifySvc.error(
this.i18nSvc.fanyi('common.notice'),
this.i18nSvc.fanyi('common.notify.import-fail-detail', { taskName: msg.taskName, errMsg: msg.errMsg })
);
} else if (msg.notifyLevel === 'INFO') {
this.notifySvc.info(
this.i18nSvc.fanyi('common.notice'),
this.i18nSvc.fanyi('common.notify.import-progress', { taskName: msg.taskName, progress: msg.progress })
);
} else {
console.error('Parse message error, msg:', evt.data);
}
});
this.eventSource.onerror = error => {
console.error('Manager SSE connection error:', error);
this.eventSource.close();
};
}
}