Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4026407
Update develop from main (#322)
bp-cos Sep 4, 2025
52c5896
Feat(8653): Implement view tracking for registrations and preprints (…
opaduchak Sep 4, 2025
6ff10ff
[ENG-8624] feat(registries): add context to registration submission +…
adlius Sep 4, 2025
0fed748
[ENG-8504] Show Osf introduction video and Collections,Institutions, …
mkovalua Sep 4, 2025
5bbf4b8
Fix/develop conflicts (#332)
nsemets Sep 5, 2025
c75d7b7
[ENG-8639] add <meta> tags to files detail (#324)
aaxelb Sep 9, 2025
4b9b2cb
feat(ci): separate linting from tests (#345)
aaxelb Sep 10, 2025
74d04f8
Feat(datacite-tracker): implemented file view and download tracking …
opaduchak Sep 10, 2025
d371a91
Feat(ENG-8778): Implement Cookie consent message (#353)
opaduchak Sep 10, 2025
4bebc78
[eng-8741] Added Sentry to the app (#340)
bp-cos Sep 11, 2025
58bae1f
Fix/dev to main (#368)
nsemets Sep 11, 2025
c54da35
feat(maintenance-banner): create maintenance banner component
bodintsov Sep 9, 2025
b629eab
fix(maintenance-banner): fix import
bodintsov Sep 9, 2025
f6cfe2c
fix(maintenance-banner): Use PrimeNG for banner
bodintsov Sep 10, 2025
b445293
fix(maintenance-banner): Created service and model for Maintenance ba…
bodintsov Sep 11, 2025
302910a
fix(maintenance-banner): Made banner show on all pages
bodintsov Sep 11, 2025
e72bc39
fix(maintenance-banner): Created new model, cleaned code
bodintsov Sep 12, 2025
38b2bbe
Merge remote-tracking branch 'angular-osf/main' into feat/add-mainten…
bodintsov Sep 12, 2025
061f81f
fix(maintenance-banner): Removed litter
bodintsov Sep 12, 2025
e7cb354
fix(maintenance-banner): Changed ApiURL, added type
bodintsov Sep 12, 2025
f64cecc
fix(maintenance-banner): Fix fetch when cookies
bodintsov Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/core/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { BreadcrumbComponent } from './breadcrumb/breadcrumb.component';
export { FooterComponent } from './footer/footer.component';
export { HeaderComponent } from './header/header.component';
export { MaintenanceBannerComponent } from './maintenance-banner/maintenance-banner.component';
export { PageNotFoundComponent } from './page-not-found/page-not-found.component';
export { RootComponent } from './root/root.component';
export { SidenavComponent } from './sidenav/sidenav.component';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@if (maintenance && !dismissed) {
<p-message
styleClass="w-full"
[severity]="maintenance.severity"
[text]="maintenance.message"
[closable]="true"
(onClose)="dismiss()"
class="block p-3"
icon="pi pi-info-circle"
>
</p-message>
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { CookieService } from 'ngx-cookie-service';

import { MessageModule } from 'primeng/message';

import { of } from 'rxjs';

import { HttpClient } from '@angular/common/http';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { MaintenanceBannerComponent } from './maintenance-banner.component';

describe('MaintenanceBannerComponent', () => {
let fixture: ComponentFixture<MaintenanceBannerComponent>;
let httpClient: { get: jest.Mock };
let cookieService: jest.Mocked<CookieService>;

beforeEach(async () => {
cookieService = {
check: jest.fn(),
set: jest.fn(),
} as any;
httpClient = { get: jest.fn() } as any;
await TestBed.configureTestingModule({
imports: [MaintenanceBannerComponent, NoopAnimationsModule, MessageModule],
providers: [
{ provide: CookieService, useValue: cookieService },
{ provide: HttpClient, useValue: httpClient },
],
}).compileComponents();

fixture = TestBed.createComponent(MaintenanceBannerComponent);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should render info banner when maintenance data is present', fakeAsync(() => {
cookieService.check.mockReturnValue(false);
const now = new Date();
const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
httpClient.get.mockReturnValueOnce(
of({
maintenance: { level: 1, message: 'Info message', start, end },
})
);
fixture.detectChanges();
tick();
fixture.detectChanges();
const banner = fixture.debugElement.query(By.css('p-message'));
expect(banner).toBeTruthy();
expect(banner.componentInstance.severity).toBe('info');
expect(banner.nativeElement.textContent).toContain('Info message');
}));

it('should render warning banner when level is 2', fakeAsync(() => {
cookieService.check.mockReturnValue(false);
const now = new Date();
const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
httpClient.get.mockReturnValueOnce(
of({
maintenance: {
level: 2,
message: 'Warning message',
start,
end,
},
})
);
fixture.detectChanges();
tick();
fixture.detectChanges();
const banner = fixture.debugElement.query(By.css('p-message'));
expect(banner).toBeTruthy();
expect(banner.componentInstance.severity).toBe('warn');
expect(banner.nativeElement.textContent).toContain('Warning message');
}));

it('should render danger banner when level is 3', fakeAsync(() => {
cookieService.check.mockReturnValue(false);
const now = new Date();
const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
httpClient.get.mockReturnValueOnce(
of({
maintenance: {
level: 3,
message: 'Danger message',
start,
end,
},
})
);
fixture.detectChanges();
tick();
fixture.detectChanges();
const banner = fixture.debugElement.query(By.css('p-message'));
expect(banner).toBeTruthy();
expect(banner.componentInstance.severity).toBe('error');
expect(banner.nativeElement.textContent).toContain('Danger message');
}));

it('should not render banner if cookie is set', fakeAsync(() => {
cookieService.check.mockReturnValue(true);
fixture.detectChanges();
expect(httpClient.get).not.toHaveBeenCalled();
fixture.detectChanges();
const banner = fixture.debugElement.query(By.css('p-message'));
expect(banner).toBeFalsy();
}));

it('should not render banner if outside maintenance window', fakeAsync(() => {
cookieService.check.mockReturnValue(false);
httpClient.get.mockReturnValueOnce(
of({
maintenance: { level: 1, message: 'Old message', start: '2020-01-01T00:00:00Z', end: '2020-01-02T00:00:00Z' },
})
);
fixture.detectChanges();
tick();
fixture.detectChanges();
const banner = fixture.debugElement.query(By.css('p-message'));
expect(banner).toBeFalsy();
}));

it('should dismiss banner when close button is clicked', fakeAsync(() => {
cookieService.check.mockReturnValue(false);
const now = new Date();
const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
httpClient.get.mockReturnValueOnce(
of({
maintenance: { level: 1, message: 'Dismiss me', start, end },
})
);
fixture.detectChanges();
tick();
fixture.detectChanges();
const banner = fixture.debugElement.query(By.css('p-message'));
expect(banner).toBeTruthy();
banner.triggerEventHandler('onClose', {});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('p-message'))).toBeFalsy();
expect(cookieService.set).toHaveBeenCalled();
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CookieService } from 'ngx-cookie-service';

import { MessageModule } from 'primeng/message';

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';

import { Maintenance } from '../../models/maintenance.model';
import { MaintenanceService } from '../../services/maintenance.service';

@Component({
selector: 'osf-maintenance-banner',
imports: [CommonModule, MessageModule],
templateUrl: './maintenance-banner.component.html',
styleUrls: ['./maintenance-banner.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MaintenanceBannerComponent implements OnInit {
private readonly maintenanceService = inject(MaintenanceService);
private readonly cookies = inject(CookieService);
private readonly cdr = inject(ChangeDetectorRef);

dismissed = false;
readonly cookieName = 'osf-maintenance-dismissed';
readonly cookieDurationHours = 24;

maintenance: Maintenance | null = null;

ngOnInit(): void {
this.dismissed = this.cookies.check(this.cookieName);
if (!this.dismissed) {
this.fetchMaintenanceStatus();
}
}

private fetchMaintenanceStatus(): void {
this.maintenanceService.fetchMaintenanceStatus().subscribe((maintenance) => {
this.maintenance = maintenance;
this.cdr.markForCheck();
});
}

dismiss(): void {
this.cookies.set(this.cookieName, '1', this.cookieDurationHours, '/');
this.dismissed = true;
this.maintenance = null;
}
}
2 changes: 2 additions & 0 deletions src/app/core/components/root/root.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<osf-sidenav></osf-sidenav>

<div class="content-wrapper">
<osf-maintenance-banner></osf-maintenance-banner>
<osf-header></osf-header>

<router-outlet />
Expand All @@ -13,6 +14,7 @@
} @else {
<div class="layout-tablet">
<div class="content-wrapper">
<osf-maintenance-banner></osf-maintenance-banner>
<osf-topnav></osf-topnav>

@if (isMedium()) {
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/components/root/root.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterOutlet } from '@angular/router';

import { SidenavComponent } from '@core/components';
import { MaintenanceBannerComponent, SidenavComponent } from '@core/components';
import { BreadcrumbComponent } from '@core/components/breadcrumb/breadcrumb.component';
import { FooterComponent } from '@core/components/footer/footer.component';
import { HeaderComponent } from '@core/components/header/header.component';
Expand All @@ -25,6 +25,7 @@ import { IS_MEDIUM, IS_WEB } from '@osf/shared/helpers';
BreadcrumbComponent,
RouterOutlet,
SidenavComponent,
MaintenanceBannerComponent,
TranslatePipe,
],
templateUrl: './root.component.html',
Expand Down
9 changes: 9 additions & 0 deletions src/app/core/models/maintenance.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Maintenance {
level: number;
message: string;
start: string;
end: string;
severity?: MaintenanceSeverity;
}

export type MaintenanceSeverity = 'info' | 'warn' | 'error';
39 changes: 39 additions & 0 deletions src/app/core/services/maintenance.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { catchError, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';

import { Maintenance, MaintenanceSeverity } from '../models/maintenance.model';

import { environment } from 'src/environments/environment';

@Injectable({ providedIn: 'root' })
export class MaintenanceService {
private readonly http = inject(HttpClient);
private readonly apiUrl = `${environment.apiDomainUrl}/v2`;

fetchMaintenanceStatus(): Observable<Maintenance | null> {
return this.http.get<{ maintenance?: Maintenance }>(`${this.apiUrl}/status/`).pipe(
map((data) => {
const maintenance = data.maintenance;
if (maintenance && this.isWithinMaintenanceWindow(maintenance)) {
return { ...maintenance, severity: this.getSeverity(maintenance.level) };
}
return null;
}),
catchError(() => of(null))
);
}

getSeverity(level: number): MaintenanceSeverity {
const map: Record<number, MaintenanceSeverity> = { 1: 'info', 2: 'warn', 3: 'error' };
return map[level] ?? 'info';
}

private isWithinMaintenanceWindow(maintenance: Maintenance): boolean {
if (!maintenance.start || !maintenance.end) return false;
const now = new Date();
return now >= new Date(maintenance.start) && now <= new Date(maintenance.end);
}
}
Loading