diff --git a/src/app/_layout/app-header/app-header.component.html b/src/app/_layout/app-header/app-header.component.html index d2107ef1ea..52e6e98b0d 100644 --- a/src/app/_layout/app-header/app-header.component.html +++ b/src/app/_layout/app-header/app-header.component.html @@ -209,4 +209,8 @@
+ diff --git a/src/app/_layout/app-header/app-header.component.ts b/src/app/_layout/app-header/app-header.component.ts index 836b9f07ca..b0afe868d4 100644 --- a/src/app/_layout/app-header/app-header.component.ts +++ b/src/app/_layout/app-header/app-header.component.ts @@ -27,6 +27,7 @@ import { map, Observable, Subscription } from "rxjs"; }) export class AppHeaderComponent implements OnInit { private sub: Subscription; + showStatusBanner: boolean; config = this.appConfigService.getConfig(); facility = this.config.facility ?? ""; @@ -89,6 +90,8 @@ export class AppHeaderComponent implements OnInit { this.isSiteHeaderLogoUrlExternal$ = this.siteHeaderLogoUrl$.pipe( map((siteHeaderLogoUrl) => this.isFullUrl(siteHeaderLogoUrl)), ); + + this.showStatusBanner = !!this.config.statusBannerMessage; } logout(): void { diff --git a/src/app/_layout/app-header/status-banner/status-banner.component.html b/src/app/_layout/app-header/status-banner/status-banner.component.html new file mode 100644 index 0000000000..3c3bb58f40 --- /dev/null +++ b/src/app/_layout/app-header/status-banner/status-banner.component.html @@ -0,0 +1,12 @@ + + + + {{ code === "INFO" ? "info" : "warning" }} + + + + + + diff --git a/src/app/_layout/app-header/status-banner/status-banner.component.scss b/src/app/_layout/app-header/status-banner/status-banner.component.scss new file mode 100644 index 0000000000..5e3b45ce35 --- /dev/null +++ b/src/app/_layout/app-header/status-banner/status-banner.component.scss @@ -0,0 +1,37 @@ +.spacer { + flex: 1 1 auto; +} + +.toolbar { + height: fit-content; + border: none; + box-shadow: none; +} + +.status-message { + text-align: center; + text-wrap: wrap; + width: 100%; + font-size: 16px; + line-height: 20px; + padding: 10px 0; +} +.status-icon { + margin-right: 6px; + font-size: 20px; + vertical-align: middle; +} +.toolbar-WARN { + background-color: #feefb3; + color: #9f6000; +} + +.toolbar-INFO { + color: #4f8a10; + background-color: #dff2bf; +} + +:host ::ng-deep a { + text-decoration: underline; + color: inherit; +} diff --git a/src/app/_layout/app-header/status-banner/status-banner.component.spec.ts b/src/app/_layout/app-header/status-banner/status-banner.component.spec.ts new file mode 100644 index 0000000000..b4c8430574 --- /dev/null +++ b/src/app/_layout/app-header/status-banner/status-banner.component.spec.ts @@ -0,0 +1,61 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { StatusBannerComponent } from "./status-banner.component"; +import { CommonModule } from "@angular/common"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { MatToolbarModule } from "@angular/material/toolbar"; +import { AppConfigInterface, AppConfigService } from "app-config.service"; + +describe("StatusBannerComponent", () => { + let component: StatusBannerComponent; + let fixture: ComponentFixture; + let appConfigServiceSpy: jasmine.SpyObj; + + beforeEach(async () => { + appConfigServiceSpy = jasmine.createSpyObj("AppConfigService", [ + "getConfig", + ]); + appConfigServiceSpy.getConfig.and.returnValue({ + statusBannerMessage: "test", + statusBannerCode: "INFO", + } as AppConfigInterface); + await TestBed.configureTestingModule({ + declarations: [StatusBannerComponent], + imports: [CommonModule, MatToolbarModule, MatButtonModule, MatIconModule], + providers: [ + { + provide: AppConfigService, + useValue: appConfigServiceSpy, + }, + ], + }).compileComponents(); + }); + + function initComponent() { + fixture = TestBed.createComponent(StatusBannerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + } + + it("should create", () => { + initComponent(); + expect(component).toBeTruthy(); + }); + + it("should set the status banner message and code from the app config", () => { + initComponent(); + expect(component.message).toBe("test"); + expect(component.code).toBe("INFO"); + }); + + it("should set default code to INFO when neither INFO or WARN is provided", () => { + appConfigServiceSpy.getConfig.and.returnValue({ + statusBannerMessage: "test123", + statusBannerCode: "ERROR", + } as unknown as AppConfigInterface); + initComponent(); + expect(component.code).toBe("INFO"); + expect(component.message).toBe("test123"); + }); +}); diff --git a/src/app/_layout/app-header/status-banner/status-banner.component.ts b/src/app/_layout/app-header/status-banner/status-banner.component.ts new file mode 100644 index 0000000000..2bf43ee93b --- /dev/null +++ b/src/app/_layout/app-header/status-banner/status-banner.component.ts @@ -0,0 +1,28 @@ +import { Component, Output, EventEmitter } from "@angular/core"; +import { AppConfigService } from "app-config.service"; + +@Component({ + selector: "app-status-banner", + templateUrl: "./status-banner.component.html", + styleUrls: ["./status-banner.component.scss"], + standalone: false, +}) +export class StatusBannerComponent { + @Output() dismiss = new EventEmitter(); + message: string; + code: "INFO" | "WARN"; + + constructor(private appConfigService: AppConfigService) { + const appConfig = this.appConfigService.getConfig(); + this.message = appConfig.statusBannerMessage; + if (["INFO", "WARN"].includes(appConfig.statusBannerCode)) { + this.code = appConfig.statusBannerCode; + } else { + this.code = "INFO"; + } + } + + onDismiss() { + this.dismiss.emit(); + } +} diff --git a/src/app/_layout/layout.module.ts b/src/app/_layout/layout.module.ts index ea544d467b..5bac31ee29 100644 --- a/src/app/_layout/layout.module.ts +++ b/src/app/_layout/layout.module.ts @@ -13,10 +13,12 @@ import { BatchCardModule } from "datasets/batch-card/batch-card.module"; import { BreadcrumbModule } from "shared/modules/breadcrumb/breadcrumb.module"; import { UsersModule } from "../users/users.module"; import { MatChipsModule } from "@angular/material/chips"; +import { StatusBannerComponent } from "./app-header/status-banner/status-banner.component"; import { MatTooltipModule } from "@angular/material/tooltip"; @NgModule({ declarations: [ + StatusBannerComponent, AppLayoutComponent, AppHeaderComponent, AppMainLayoutComponent, diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json index e57e631d2b..336c7e57a0 100644 --- a/src/app/admin/schema/frontend.config.jsonforms.json +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -99,6 +99,12 @@ "metadataEditingUnitListDisabled": { "type": "boolean" }, "hideEmptyMetadataTable": { "type": "boolean" }, "datafilesActionsEnabled": { "type": "boolean" }, + "statusBannerMessage": { "type": "string" }, + "statusBannerCode": { + "type": "string", + "enum": ["INFO", "WARN"], + "default": "INFO" + }, "datafilesActions": { "type": "array", "items": { @@ -409,6 +415,8 @@ { "type": "Control", "scope": "#/properties/siteIcon" }, { "type": "Control", "scope": "#/properties/siteTitle" }, { "type": "Control", "scope": "#/properties/siteSciCatLogo" }, + { "type": "Control", "scope": "#/properties/statusBannerMessage" }, + { "type": "Control", "scope": "#/properties/statusBannerCode" }, { "type": "Control", "scope": "#/properties/checkBoxFilterClickTrigger" diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 1ba2952809..7debb96695 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -167,6 +167,8 @@ export interface AppConfigInterface { hideEmptyMetadataTable?: boolean; ingestorComponent?: IngestorComponentConfig; defaultTab?: DefaultTab; + statusBannerMessage?: string; + statusBannerCode?: "INFO" | "WARN"; } function isMainPageConfiguration(obj: any): obj is MainPageConfiguration { @@ -275,7 +277,6 @@ export class AppConfigService { if (!this.appConfig) { console.error("AppConfigService: Configuration not loaded!"); } - return this.appConfig as AppConfigInterface; } }