Skip to content

Commit fbffe1d

Browse files
authored
Merge pull request ceph#64375 from SundownRises/header-component
mgr/dashboard: Carbonised Notification Header Reviewed-by: Afreen Misbah <[email protected]>
2 parents d050b32 + c618d44 commit fbffe1d

16 files changed

+410
-36
lines changed

src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CommonModule } from '@angular/common';
2-
import { NgModule } from '@angular/core';
2+
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
33
import { RouterModule } from '@angular/router';
44

55
import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@@ -12,7 +12,9 @@ import {
1212
DialogModule,
1313
GridModule,
1414
BreadcrumbModule,
15-
ModalModule
15+
ModalModule,
16+
ToggleModule,
17+
ButtonModule
1618
} from 'carbon-components-angular';
1719

1820
import { AppRoutingModule } from '~/app/app-routing.module';
@@ -26,6 +28,8 @@ import { DashboardHelpComponent } from './dashboard-help/dashboard-help.componen
2628
import { IdentityComponent } from './identity/identity.component';
2729
import { NavigationComponent } from './navigation/navigation.component';
2830
import { NotificationsComponent } from './notifications/notifications.component';
31+
import { NotificationPanelComponent } from './notification-panel/notification-panel.component';
32+
import { NotificationHeaderComponent } from './notification-panel/header/notification-header.component';
2933

3034
// Icons
3135
import UserFilledIcon from '@carbon/icons/es/user--filled/20';
@@ -61,20 +65,25 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
6165
DialogModule,
6266
GridModule,
6367
BreadcrumbModule,
64-
ModalModule
68+
ModalModule,
69+
ToggleModule,
70+
ButtonModule
6571
],
6672
declarations: [
6773
AboutComponent,
6874
ApiDocsComponent,
6975
BreadcrumbsComponent,
7076
NavigationComponent,
7177
NotificationsComponent,
78+
NotificationPanelComponent,
79+
NotificationHeaderComponent,
7280
DashboardHelpComponent,
7381
AdministrationComponent,
7482
IdentityComponent
7583
],
76-
providers: [ModalCdsService],
77-
exports: [NavigationComponent, BreadcrumbsComponent]
84+
exports: [NavigationComponent, BreadcrumbsComponent],
85+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
86+
providers: [ModalCdsService]
7887
})
7988
export class NavigationModule {
8089
constructor(private iconService: IconService) {

src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export class NavigationComponent implements OnInit, OnDestroy {
209209
);
210210
}
211211
toggleSidebar() {
212-
this.notificationService.toggleSidebar();
212+
this.notificationService.toggleSidebar(true, true);
213213
}
214214
trackByFn(item: any) {
215215
return item;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<div class="notification-header">
2+
<div class="notification-header__top">
3+
<div class="notification-header__title">
4+
<cds-text i18n>Tasks and Notifications</cds-text>
5+
</div>
6+
7+
<button
8+
i18n
9+
cdsButton="ghost"
10+
size="sm"
11+
(click)="onDismissAll()"
12+
class="notification-header__dismiss-btn">
13+
Dismiss all
14+
</button>
15+
</div>
16+
17+
<div class="notification-header__toggle">
18+
<cds-toggle
19+
[checked]="isMuted"
20+
size="sm"
21+
(checkedChange)="onToggleMute()"
22+
label="Mute notifications"
23+
i18n-label
24+
hideLabel="true"> <!--hides the toggle state values (like "On/Off" in the toggle button)-->
25+
</cds-toggle>
26+
</div>
27+
</div>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
@use '@carbon/styles/scss/type';
2+
@use '@carbon/styles/scss/spacing';
3+
@use '@carbon/styles/scss/theme';
4+
5+
.notification-header {
6+
display: flex;
7+
flex-direction: column;
8+
padding: spacing.$spacing-04;
9+
border-bottom: 1px solid theme.$border-subtle-01;
10+
background-color: theme.$layer-01;
11+
12+
&__top {
13+
display: flex;
14+
justify-content: space-between;
15+
align-items: center;
16+
width: 100%;
17+
margin-bottom: spacing.$spacing-03;
18+
}
19+
20+
&__title {
21+
h4 {
22+
@include type.type-style('heading-compact-01');
23+
24+
color: theme.$text-primary;
25+
margin: 0;
26+
}
27+
}
28+
29+
&__dismiss-btn {
30+
color: theme.$text-primary;
31+
32+
&:hover {
33+
color: theme.$link-primary;
34+
}
35+
}
36+
37+
&__toggle {
38+
cds-toggle {
39+
margin: 0;
40+
41+
::ng-deep {
42+
.cds--toggle__label-text {
43+
color: theme.$text-primary;
44+
}
45+
46+
.cds--toggle__label {
47+
color: theme.$text-primary;
48+
}
49+
50+
.cds--toggle__text {
51+
color: theme.$text-primary;
52+
}
53+
}
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { NotificationHeaderComponent } from './notification-header.component';
3+
import { NotificationService } from '../../../../shared/services/notification.service';
4+
import { BehaviorSubject } from 'rxjs';
5+
6+
describe('NotificationHeaderComponent', () => {
7+
let component: NotificationHeaderComponent;
8+
let fixture: ComponentFixture<NotificationHeaderComponent>;
9+
let notificationService: NotificationService;
10+
let muteStateSubject: BehaviorSubject<boolean>;
11+
12+
beforeEach(async () => {
13+
muteStateSubject = new BehaviorSubject<boolean>(false);
14+
await TestBed.configureTestingModule({
15+
declarations: [NotificationHeaderComponent],
16+
providers: [
17+
{
18+
provide: NotificationService,
19+
useValue: {
20+
muteState$: muteStateSubject.asObservable(),
21+
removeAll: jasmine.createSpy('removeAll'),
22+
suspendToasties: jasmine.createSpy('suspendToasties')
23+
}
24+
}
25+
]
26+
}).compileComponents();
27+
28+
fixture = TestBed.createComponent(NotificationHeaderComponent);
29+
component = fixture.componentInstance;
30+
notificationService = TestBed.inject(NotificationService);
31+
fixture.detectChanges();
32+
});
33+
34+
it('should create', () => {
35+
expect(component).toBeTruthy();
36+
});
37+
38+
it('should initialize with default mute state', () => {
39+
expect(component.isMuted).toBe(false);
40+
});
41+
42+
it('should update mute state when subscription emits', () => {
43+
muteStateSubject.next(true);
44+
fixture.detectChanges();
45+
expect(component.isMuted).toBe(true);
46+
});
47+
48+
it('should emit dismissAll event and call removeAll on dismiss', () => {
49+
spyOn(component.dismissAll, 'emit');
50+
51+
component.onDismissAll();
52+
53+
expect(component.dismissAll.emit).toHaveBeenCalled();
54+
expect(notificationService.removeAll).toHaveBeenCalled();
55+
});
56+
57+
it('should toggle mute state', () => {
58+
component.isMuted = false;
59+
component.onToggleMute();
60+
expect(notificationService.suspendToasties).toHaveBeenCalledWith(true);
61+
62+
component.isMuted = true;
63+
component.onToggleMute();
64+
expect(notificationService.suspendToasties).toHaveBeenCalledWith(false);
65+
});
66+
67+
it('should unsubscribe on destroy', () => {
68+
spyOn(component['subs'], 'unsubscribe');
69+
component.ngOnDestroy();
70+
expect(component['subs'].unsubscribe).toHaveBeenCalled();
71+
});
72+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
2+
import { NotificationService } from '../../../../shared/services/notification.service';
3+
import { Subscription } from 'rxjs';
4+
5+
@Component({
6+
selector: 'cd-notification-header',
7+
templateUrl: './notification-header.component.html',
8+
styleUrls: ['./notification-header.component.scss']
9+
})
10+
export class NotificationHeaderComponent implements OnInit, OnDestroy {
11+
@Output() dismissAll = new EventEmitter<void>();
12+
13+
isMuted = false;
14+
private subs = new Subscription();
15+
16+
constructor(private notificationService: NotificationService) {}
17+
18+
ngOnInit(): void {
19+
this.subs.add(
20+
this.notificationService.muteState$.subscribe((isMuted) => {
21+
this.isMuted = isMuted;
22+
})
23+
);
24+
}
25+
26+
ngOnDestroy(): void {
27+
this.subs.unsubscribe();
28+
}
29+
30+
onDismissAll(): void {
31+
this.dismissAll.emit();
32+
this.notificationService.removeAll();
33+
}
34+
35+
onToggleMute(): void {
36+
this.notificationService.suspendToasties(!this.isMuted);
37+
}
38+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="notification-panel">
2+
<cd-notification-header></cd-notification-header>
3+
<!-- Body and footer components will be added here later -->
4+
</div>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@use '@carbon/styles/scss/theme';
2+
@use '@carbon/styles/scss/spacing';
3+
@use '@carbon/styles/scss/themes';
4+
@use '@carbon/styles/scss/theme' as *;
5+
6+
.notification-panel {
7+
@include theme.theme(themes.$g10);
8+
9+
position: absolute;
10+
top: spacing.$spacing-09;
11+
right: 0;
12+
width: 400px; // Keep original width as it doesn't map to a spacing token
13+
background-color: $layer-01;
14+
box-shadow: $shadow;
15+
border: 1px solid $border-subtle-01;
16+
z-index: 6000;
17+
color: $text-primary;
18+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { NotificationPanelComponent } from './notification-panel.component';
3+
import { NotificationService } from '../../../shared/services/notification.service';
4+
5+
describe('NotificationPanelComponent', () => {
6+
let component: NotificationPanelComponent;
7+
let fixture: ComponentFixture<NotificationPanelComponent>;
8+
let notificationService: NotificationService;
9+
10+
beforeEach(async () => {
11+
await TestBed.configureTestingModule({
12+
declarations: [NotificationPanelComponent],
13+
providers: [
14+
{
15+
provide: NotificationService,
16+
useValue: {
17+
toggleSidebar: jasmine.createSpy('toggleSidebar')
18+
}
19+
}
20+
]
21+
}).compileComponents();
22+
});
23+
24+
beforeEach(() => {
25+
fixture = TestBed.createComponent(NotificationPanelComponent);
26+
component = fixture.componentInstance;
27+
notificationService = TestBed.inject(NotificationService);
28+
fixture.detectChanges();
29+
});
30+
31+
it('should create', () => {
32+
expect(component).toBeTruthy();
33+
});
34+
35+
describe('handleClickOutside', () => {
36+
it('should close sidebar when clicking outside', () => {
37+
// Create a click event outside the component
38+
const outsideClickEvent = new MouseEvent('click', {
39+
bubbles: true,
40+
cancelable: true
41+
});
42+
document.dispatchEvent(outsideClickEvent);
43+
44+
expect(notificationService.toggleSidebar).toHaveBeenCalledWith(false, true);
45+
});
46+
47+
it('should not close sidebar when clicking inside', () => {
48+
// Create a click event inside the component
49+
const insideClickEvent = new MouseEvent('click', {
50+
bubbles: true,
51+
cancelable: true
52+
});
53+
54+
const componentElement = fixture.nativeElement;
55+
componentElement.dispatchEvent(insideClickEvent);
56+
57+
expect(notificationService.toggleSidebar).not.toHaveBeenCalled();
58+
});
59+
});
60+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Component, ElementRef, HostListener } from '@angular/core';
2+
import { NotificationService } from '../../../shared/services/notification.service';
3+
4+
@Component({
5+
selector: 'cd-notification-panel',
6+
templateUrl: './notification-panel.component.html',
7+
styleUrls: ['./notification-panel.component.scss']
8+
})
9+
export class NotificationPanelComponent {
10+
constructor(public notificationService: NotificationService, private elementRef: ElementRef) {}
11+
12+
@HostListener('document:click', ['$event'])
13+
handleClickOutside(event: Event) {
14+
const clickedInside = this.elementRef.nativeElement.contains(event.target);
15+
if (!clickedInside) {
16+
this.notificationService.toggleSidebar(false, true);
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)