Skip to content

Commit 9b6bb97

Browse files
authored
[ENG-11938] Start maintenance mode detection upon app startup - #1038
1 parent ee63e88 commit 9b6bb97

9 files changed

Lines changed: 62 additions & 21 deletions

File tree

‎src/app/app.component.html‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
<router-outlet />
22
<osf-toast></osf-toast>
33
<osf-full-screen-loader></osf-full-screen-loader>
4+
5+
@if (isMaintenanceMode()) {
6+
<section class="maintenance-overlay font-bold text-xl flex flex-column align-items-center justify-content-center">
7+
<p>{{ 'maintenance.title' | translate }}</p>
8+
<p>{{ 'maintenance.message' | translate }}</p>
9+
</section>
10+
}

‎src/app/app.component.scss‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.maintenance-overlay {
2+
position: fixed;
3+
inset: 0;
4+
z-index: 2000;
5+
background: var(--white);
6+
}

‎src/app/app.component.spec.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
99
import { NavigationEnd, ResolveStart, Router } from '@angular/router';
1010

1111
import { ENVIRONMENT } from '@core/provider/environment.provider';
12+
import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
1213
import { GetCurrentUser } from '@core/store/user';
1314
import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails';
1415
import { AccountEmailModel } from '@osf/shared/models/emails/account-email.model';
@@ -17,6 +18,10 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'
1718
import { provideOSFCore } from '@testing/osf.testing.provider';
1819
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
1920
import { LoaderServiceMock, provideLoaderServiceMock } from '@testing/providers/loader-service.mock';
21+
import {
22+
MaintenanceModeServiceMock,
23+
MaintenanceModeServiceMockType,
24+
} from '@testing/providers/maintenance-mode.service.mock';
2025
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
2126
import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock';
2227

@@ -36,6 +41,7 @@ describe('AppComponent', () => {
3641
let loaderServiceMock: LoaderServiceMock;
3742
let customDialogServiceMock: ReturnType<CustomDialogServiceMockBuilder['build']>;
3843
let gtmServiceMock: { pushTag: Mock };
44+
let maintenanceModeServiceMock: MaintenanceModeServiceMockType;
3945

4046
const unverifiedEmail: AccountEmailModel = {
4147
id: 'email-1',
@@ -50,6 +56,7 @@ describe('AppComponent', () => {
5056
isBrowser?: boolean;
5157
unverifiedEmails?: AccountEmailModel[];
5258
googleTagManagerId?: string;
59+
isMaintenanceMode?: boolean;
5360
}
5461

5562
function setup(overrides: SetupOverrides = {}) {
@@ -58,6 +65,7 @@ describe('AppComponent', () => {
5865
loaderServiceMock = new LoaderServiceMock();
5966
customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build();
6067
gtmServiceMock = { pushTag: vi.fn() };
68+
maintenanceModeServiceMock = MaintenanceModeServiceMock.simple(overrides.isMaintenanceMode ?? false);
6169

6270
TestBed.configureTestingModule({
6371
imports: [AppComponent, ...MockComponents(ToastComponent, FullScreenLoaderComponent)],
@@ -67,6 +75,7 @@ describe('AppComponent', () => {
6775
MockProvider(Router, routerMock),
6876
MockProvider(CustomDialogService, customDialogServiceMock),
6977
MockProvider(GoogleTagManagerService, gtmServiceMock),
78+
MockProvider(MaintenanceModeService, maintenanceModeServiceMock),
7079
MockProvider(PLATFORM_ID, overrides.isBrowser === false ? 'server' : 'browser'),
7180
provideMockStore({
7281
signals: mergeSignalOverrides(
@@ -149,6 +158,21 @@ describe('AppComponent', () => {
149158
});
150159
});
151160

161+
it('should probe maintenance mode once on init in browser', () => {
162+
setup();
163+
expect(maintenanceModeServiceMock.checkOnce).toHaveBeenCalledTimes(1);
164+
});
165+
166+
it('should render the maintenance overlay when maintenance mode is active', () => {
167+
setup({ isMaintenanceMode: true });
168+
expect(fixture.nativeElement.querySelector('.maintenance-overlay')).toBeTruthy();
169+
});
170+
171+
it('should not render the maintenance overlay when maintenance mode is inactive', () => {
172+
setup();
173+
expect(fixture.nativeElement.querySelector('.maintenance-overlay')).toBeFalsy();
174+
});
175+
152176
it('should not subscribe to router events on server', () => {
153177
setup({ isBrowser: false });
154178
routerBuilder.emit(new ResolveStart(1, '/next', '/next', {} as any));

‎src/app/app.component.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createDispatchMap, select } from '@ngxs/store';
22

3+
import { TranslatePipe } from '@ngx-translate/core';
4+
35
import { switchMap, timer } from 'rxjs';
46

57
import { isPlatformBrowser } from '@angular/common';
@@ -8,6 +10,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
810
import { NavigationCancel, NavigationEnd, NavigationError, ResolveStart, Router, RouterOutlet } from '@angular/router';
911

1012
import { ENVIRONMENT } from '@core/provider/environment.provider';
13+
import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
1114
import { GetCurrentUser } from '@core/store/user';
1215
import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails';
1316

@@ -21,7 +24,7 @@ import { GoogleTagManagerService } from 'angular-google-tag-manager';
2124

2225
@Component({
2326
selector: 'osf-root',
24-
imports: [RouterOutlet, ToastComponent, FullScreenLoaderComponent],
27+
imports: [RouterOutlet, ToastComponent, FullScreenLoaderComponent, TranslatePipe],
2528
templateUrl: './app.component.html',
2629
styleUrl: './app.component.scss',
2730
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -36,8 +39,10 @@ export class AppComponent implements OnInit {
3639
private readonly platformId = inject(PLATFORM_ID);
3740
private readonly isBrowser = isPlatformBrowser(this.platformId);
3841
private readonly loaderService = inject(LoaderService);
42+
private readonly maintenanceModeService = inject(MaintenanceModeService);
3943

4044
unverifiedEmails = select(UserEmailsSelectors.getUnverifiedEmails);
45+
isMaintenanceMode = this.maintenanceModeService.isActive;
4146

4247
constructor() {
4348
effect(() => {
@@ -57,6 +62,9 @@ export class AppComponent implements OnInit {
5762
.subscribe();
5863

5964
if (this.isBrowser) {
65+
// `GetCurrentUser` short-circuits on a cached user, so it cannot be relied on to surface an outage.
66+
this.maintenanceModeService.checkOnce();
67+
6068
this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
6169
if (event instanceof ResolveStart) {
6270
this.loaderService.show();

‎src/app/core/components/layout/layout.component.html‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@
1919

2020
<osf-footer></osf-footer>
2121
</div>
22-
23-
@if (isMaintenanceMode()) {
24-
<section class="maintenance-overlay font-bold text-xl flex flex-column align-items-center justify-content-center">
25-
<p>{{ 'maintenance.title' | translate }}</p>
26-
<p>{{ 'maintenance.message' | translate }}</p>
27-
</section>
28-
}
2922
</main>
3023

3124
<p-confirm-dialog

‎src/app/core/components/layout/layout.component.scss‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,3 @@
8484
}
8585
}
8686
}
87-
88-
.maintenance-overlay {
89-
position: fixed;
90-
inset: 0;
91-
z-index: 2000;
92-
background: var(--white);
93-
}

‎src/app/core/components/layout/layout.component.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
77
import { RouterOutlet } from '@angular/router';
88

9-
import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
109
import { ScrollTopOnRouteChangeDirective } from '@osf/shared/directives/scroll-top.directive';
1110
import { IS_MEDIUM, IS_WEB } from '@osf/shared/helpers/breakpoints.tokens';
1211

@@ -36,9 +35,6 @@ import { TopnavComponent } from '../topnav/topnav.component';
3635
changeDetection: ChangeDetectionStrategy.OnPush,
3736
})
3837
export class LayoutComponent {
39-
private readonly maintenanceModeService = inject(MaintenanceModeService);
40-
4138
isWeb = toSignal(inject(IS_WEB));
4239
isMedium = toSignal(inject(IS_MEDIUM));
43-
isMaintenanceMode = this.maintenanceModeService.isActive;
4440
}

‎src/app/core/services/maintenance-mode.service.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ export class MaintenanceModeService implements OnDestroy {
2323

2424
readonly isActive = this._isActive.asReadonly();
2525

26+
/**
27+
* Check for maintenance mode upon application startup.
28+
* If the application is in maintenance mode, activate the service and start polling for when maintenance mode ends.
29+
*/
30+
checkOnce(): void {
31+
this.checkMaintenanceStatus().subscribe((isMaintenance) => {
32+
if (isMaintenance) {
33+
this.activate();
34+
}
35+
});
36+
}
37+
2638
activate(): void {
2739
this._isActive.set(true);
2840
if (this.pollingSubscription) {

‎src/testing/providers/maintenance-mode.service.mock.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import { MaintenanceModeService } from '@core/services/maintenance-mode.service'
77
export type MaintenanceModeServiceMockType = Partial<MaintenanceModeService> & {
88
activate: Mock<() => void>;
99
deactivate: Mock<() => void>;
10+
checkOnce: Mock<() => void>;
1011
isActive: Signal<boolean>;
1112
};
1213

1314
export const MaintenanceModeServiceMock = {
14-
simple() {
15+
simple(isActive = false) {
1516
return {
1617
activate: vi.fn(),
1718
deactivate: vi.fn(),
18-
isActive: signal(false).asReadonly(),
19+
checkOnce: vi.fn(),
20+
isActive: signal(isActive).asReadonly(),
1921
} as MaintenanceModeServiceMockType;
2022
},
2123
};

0 commit comments

Comments
 (0)