Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

86 frontend alert panel component deactivate option #117

Merged
merged 11 commits into from
Dec 3, 2024
26 changes: 16 additions & 10 deletions apps/frontend/src/app/alert/component/alert.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,32 @@ export class AlertComponent implements OnInit {

ngOnInit(): void {
this.loadAlerts();
this.alertService
.getRefreshObservable()
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.loadAlerts();
});
}

loadAlerts(): void {
this.alertService
.getAllAlerts(this.DAYS)
.pipe(takeUntil(this.destroy$))
.subscribe((data: Alert[]) => {
const criticalAlerts = data.filter(
(alert) => alert.alertType.severity === SeverityType.CRITICAL
const activeAlerts = data.filter(
(alert) => alert.alertType.user_active === true
);
const warningAlerts = data.filter(
this.criticalAlertsCount = activeAlerts.filter(
(alert) => alert.alertType.severity === SeverityType.CRITICAL
).length;
this.warningAlertsCount = activeAlerts.filter(
(alert) => alert.alertType.severity === SeverityType.WARNING
);
const infoAlerts = data.filter(
).length;
this.infoAlertsCount = activeAlerts.filter(
(alert) => alert.alertType.severity === SeverityType.INFO
);
this.criticalAlertsCount = criticalAlerts.length;
this.warningAlertsCount = warningAlerts.length;
this.infoAlertsCount = infoAlerts.length;
this.alerts = [...criticalAlerts, ...warningAlerts, ...infoAlerts];
).length;
this.alerts = activeAlerts;
this.status = this.getStatus();
});
}
Expand Down
11 changes: 10 additions & 1 deletion apps/frontend/src/app/alert/service/alert-service.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Inject, Injectable } from '@angular/core';
import { BASE_URL } from '../../shared/types/configuration';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import { Alert } from '../../shared/types/alert';

@Injectable({
providedIn: 'root',
})
export class AlertServiceService {
private refreshAlerts = new Subject<void>();
constructor(
@Inject(BASE_URL) private readonly baseUrl: string,
private readonly http: HttpClient
Expand All @@ -19,4 +20,12 @@ export class AlertServiceService {
}
return this.http.get<Alert[]>(`${this.baseUrl}/alerting`);
}

refresh() {
this.refreshAlerts.next();
}

getRefreshObservable() {
return this.refreshAlerts.asObservable();
}
}
17 changes: 15 additions & 2 deletions apps/frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@
<clr-header class="header">
<img src="../../../../public/team_logo.png" alt="Team Logo" width="50" />
<span class="title">Metadata Analyzer</span>
<div class="header-actions"></div>
<div class="header-actions">
<clr-dropdown>
<button class="nav-icon" clrDropdownTrigger>
<cds-icon shape="cog"></cds-icon>
</button>
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-right">
<button clrDropdownItem (click)="openNotificationSettingsModal()">
<cds-icon shape="bell"></cds-icon>
<span class="nav-text">Notifications</span>
</button>
</clr-dropdown-menu>
</clr-dropdown>
<app-notification-settings></app-notification-settings>
</div>
</clr-header>

<div class="content-container">
Expand All @@ -21,7 +34,7 @@
routerLink="/"
routerLinkActive="active"
class="nav-link"
><span class="nav-text">Overview</span></a
><span class="nav-text">Overview</span></a
>
</clr-vertical-nav-group-children>
</clr-vertical-nav-group>
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
font-size: 24px;
font-weight: bold;
}

.dropdown-menu {
--clr-header-font-color: #000;
--clr-header-font-color-hover: #000;
}
9 changes: 8 additions & 1 deletion apps/frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { map } from 'rxjs';
import { NotificationSettingsComponent } from './management/components/settings/notification-settings/notification-settings.component';

@Component({
selector: 'app-root',
Expand All @@ -8,9 +9,15 @@ import { map } from 'rxjs';
})
export class AppComponent {
title = 'metadata-analyzer-frontend';
@ViewChild(NotificationSettingsComponent)
private notificationSettings!: NotificationSettingsComponent;


constructor() {}

openNotificationSettingsModal() {
this.notificationSettings.open();
}


}
12 changes: 9 additions & 3 deletions apps/frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ import { ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular';
import {
ClarityIcons, errorStandardIcon,
ClarityIcons,
errorStandardIcon,
homeIcon,
searchIcon,
tableIcon,
uploadCloudIcon,
warningStandardIcon
warningStandardIcon,
cogIcon,
bellIcon,
} from '@cds/core/icon';
import { NgxEchartsModule } from 'ngx-echarts';
import { TestUploadComponent } from './test-upload/component/test-upload/test-upload.component';
import { FindTestDataComponent } from './test-upload/component/find-test-data/find-test-data.component';
import { BackupsComponent } from './backups-overview/backups/backups/backups.component';
import { BASE_URL } from './shared/types/configuration';
import { AlertComponent } from './alert/component/alert.component';
import { NotificationSettingsComponent } from './management/components/settings/notification-settings/notification-settings.component';

@NgModule({
declarations: [
Expand All @@ -31,6 +35,7 @@ import { AlertComponent } from './alert/component/alert.component';
FindTestDataComponent,
BackupsComponent,
AlertComponent,
NotificationSettingsComponent,
],
imports: [
BrowserModule,
Expand All @@ -55,7 +60,8 @@ export class AppModule {
tableIcon,
warningStandardIcon,
errorStandardIcon,

cogIcon,
bellIcon
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.notification-item {
margin-bottom: 1rem;
}

.badge {
margin-left: 0.5rem;
}

clr-toggle-container {
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<clr-modal
[(clrModalOpen)]="isOpen"
[clrModalClosable]="false"
clrModalSize="md"
>
<h3 class="modal-title">Notification Settings</h3>
<div class="modal-body">
<form clrForm [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
<clr-spinner *ngIf="isLoading"></clr-spinner>

<div formArrayName="notifications">
<div
*ngFor="
let notification of notificationControls.controls;
let i = index
"
[formGroupName]="i"
class="notification-item"
>
<clr-control-container>
<clr-toggle-wrapper>
<input type="checkbox" clrToggle formControlName="user_active" />
<label>{{ notification.get('name')?.value }}</label>
</clr-toggle-wrapper>
<span
class="badge"
[ngClass]="{
'badge-warning':
notification.get('severity')?.value === 'WARNING',
'badge-danger': notification.get('severity')?.value === 'ERROR',
'badge-info': notification.get('severity')?.value === 'INFO'
}"
>
{{ notification.get('severity')?.value }}
</span>
</clr-control-container>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="close()">
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
[clrLoading]="isSaving"
(click)="saveSettings()"
[disabled]="isLoading || !settingsForm.valid"
>
Save
</button>
</div>
</clr-modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FormBuilder } from '@angular/forms';
import { of, throwError } from 'rxjs';

import { NotificationSettingsComponent } from './notification-settings.component';
import { NotificationService } from '../../../services/notification.service';

describe('NotificationSettingsComponent', () => {
let component: NotificationSettingsComponent;
let mockNotificationService: {
getNotificationSettings: ReturnType<typeof vi.fn>;
updateNotificationSettings: ReturnType<typeof vi.fn>;
};
let mockFormBuilder: FormBuilder;

const mockNotificationSettings = [
{
id: '1',
name: 'Test Notification',
severity: 'high',
user_active: true,
master_active: false,
},
];

beforeEach(() => {
mockNotificationService = {
getNotificationSettings: vi.fn(),
updateNotificationSettings: vi.fn(),
};

mockFormBuilder = new FormBuilder();

component = new NotificationSettingsComponent(
mockNotificationService as any,
mockFormBuilder
);
});

describe('Unit Tests', () => {
it('should initialize with default state', () => {
expect(component.isLoading).toBe(false);
expect(component.isSaving).toBe(false);
expect(component.isOpen).toBe(false);
expect(component.settingsForm).toBeDefined();
});

it('should open and close modal correctly', () => {
component.open();
expect(component.isOpen).toBe(true);

component.close();
expect(component.isOpen).toBe(false);
});

it('should load notification settings successfully', () => {
mockNotificationService.getNotificationSettings.mockReturnValue(
of(mockNotificationSettings)
);

component.loadNotificationSettings();

expect(component.isLoading).toBe(false);
expect(component.notificationControls.length).toBe(1);
});

it('should handle error when loading notification settings', () => {
mockNotificationService.getNotificationSettings.mockReturnValue(
throwError(() => new Error('Load failed'))
);

component.loadNotificationSettings();

expect(component.isLoading).toBe(false);
});
});

describe('Integration Tests', () => {
it('should save notification settings successfully', () => {
mockNotificationService.getNotificationSettings.mockReturnValue(
of(mockNotificationSettings)
);
mockNotificationService.updateNotificationSettings.mockReturnValue(
of(mockNotificationSettings)
);

component.loadNotificationSettings();

const notificationControl = component.notificationControls.at(0);
notificationControl.patchValue({ user_active: false });

component.saveSettings();

expect(component.isLoading).toBe(false);
expect(component.isOpen).toBe(false);
});

it('should handle save settings error', () => {
mockNotificationService.getNotificationSettings.mockReturnValue(
of(mockNotificationSettings)
);
mockNotificationService.updateNotificationSettings.mockReturnValue(
throwError(() => new Error('Save failed'))
);

component.loadNotificationSettings();

const notificationControl = component.notificationControls.at(0);
notificationControl.patchValue({ user_active: false });

component.saveSettings();

expect(component.isLoading).toBe(false);
});
});

describe('Lifecycle Tests', () => {
it('should clean up on component destroy', () => {
const destroySpy = vi.spyOn(component['destroy$'], 'next');
const completeDestroySpy = vi.spyOn(component['destroy$'], 'complete');

component.ngOnDestroy();

expect(component.isOpen).toBe(false);
expect(destroySpy).toHaveBeenCalled();
expect(completeDestroySpy).toHaveBeenCalled();
});
});
});
Loading
Loading