diff --git a/src/app/modules/alerts/components/alert/alert.component.html b/src/app/modules/alerts/components/alert/alert.component.html index 5ca6d065d7c..f27c2116be4 100644 --- a/src/app/modules/alerts/components/alert/alert.component.html +++ b/src/app/modules/alerts/components/alert/alert.component.html @@ -12,7 +12,23 @@

{{ levelLabel() }}

} -

+

+ @if (isExpandable()) { + + } @if (isHaLicensed()) {
{{ alert().node }}
} diff --git a/src/app/modules/alerts/components/alert/alert.component.scss b/src/app/modules/alerts/components/alert/alert.component.scss index 8351037f578..72ef8a39d51 100644 --- a/src/app/modules/alerts/components/alert/alert.component.scss +++ b/src/app/modules/alerts/components/alert/alert.component.scss @@ -1,3 +1,5 @@ +@import 'mixins/text'; + :host { align-items: center; display: flex; @@ -48,6 +50,29 @@ line-height: inherit; margin-bottom: 6px; margin-top: 3px; + + &.collapsed { + @include line-clamp(5); + } +} + +.expand-collapse-button { + background-color: var(--alt-bg1); + cursor: pointer; + display: flex; + font-size: inherit; + font-weight: normal; + height: inherit; + justify-content: center; + line-height: inherit; + margin: 0 0 5px; + opacity: 0.9; + padding: 0; + width: max-content; + + &.collapsed { + margin-top: -8px; + } } .alert-node { diff --git a/src/app/modules/alerts/components/alert/alert.component.spec.ts b/src/app/modules/alerts/components/alert/alert.component.spec.ts index bfae7af8c5b..3bc99bd3638 100644 --- a/src/app/modules/alerts/components/alert/alert.component.spec.ts +++ b/src/app/modules/alerts/components/alert/alert.component.spec.ts @@ -1,3 +1,7 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { By } from '@angular/platform-browser'; import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { EffectsModule } from '@ngrx/effects'; import { Store, StoreModule } from '@ngrx/store'; @@ -34,6 +38,8 @@ describe('AlertComponent', () => { let spectator: Spectator; let api: ApiService; let alert: AlertPageObject; + let loader: HarnessLoader; + const createComponent = createComponentFactory({ component: AlertComponent, imports: [ @@ -73,6 +79,7 @@ describe('AlertComponent', () => { api = spectator.inject(ApiService); alert = new AlertPageObject(spectator); + loader = TestbedHarnessEnvironment.loader(spectator.fixture); }); it('shows alert level', () => { @@ -127,4 +134,23 @@ describe('AlertComponent', () => { const state = await firstValueFrom(spectator.inject(Store).pipe(map(selectAlerts))); expect(state).toEqual([dummyAlert]); }); + + it('shows expand/collapse button when alert message is too long', async () => { + const longMessage = 'This is a very long alert message '.repeat(10); + spectator.setInput('alert', { ...dummyAlert, formatted: longMessage } as Alert); + + const alertMessageElement = spectator.debugElement.query(By.css('.alert-message')).nativeElement as HTMLElement; + jest.spyOn(alertMessageElement, 'scrollHeight', 'get').mockReturnValue(300); + jest.spyOn(alertMessageElement, 'offsetHeight', 'get').mockReturnValue(100); + + spectator.component.ngAfterViewInit(); + + const expandButton = await loader.getHarness(MatButtonHarness.with({ text: 'Expand' })); + expect(expandButton).toExist(); + + await expandButton.click(); + + const collapseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Collapse' })); + expect(collapseButton).toExist(); + }); }); diff --git a/src/app/modules/alerts/components/alert/alert.component.ts b/src/app/modules/alerts/components/alert/alert.component.ts index ac95cebcb59..933ebfe773d 100644 --- a/src/app/modules/alerts/components/alert/alert.component.ts +++ b/src/app/modules/alerts/components/alert/alert.component.ts @@ -1,7 +1,11 @@ import { AsyncPipe } from '@angular/common'; import { - ChangeDetectionStrategy, Component, computed, HostBinding, input, OnChanges, + AfterViewInit, + ChangeDetectionStrategy, Component, computed, ElementRef, HostBinding, input, OnChanges, + signal, + ViewChild, } from '@angular/core'; +import { MatButton } from '@angular/material/button'; import { MatTooltip } from '@angular/material/tooltip'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; @@ -43,6 +47,7 @@ enum AlertLevelColor { imports: [ IxIconComponent, MatTooltip, + MatButton, TestDirective, TranslateModule, FormatDateTimePipe, @@ -50,10 +55,16 @@ enum AlertLevelColor { RequiresRolesDirective, ], }) -export class AlertComponent implements OnChanges { +export class AlertComponent implements OnChanges, AfterViewInit { readonly alert = input.required(); readonly isHaLicensed = input(); - readonly requiredRoles = [Role.AlertListWrite]; + + @ViewChild('alertMessage', { static: true }) alertMessage: ElementRef; + + protected isCollapsed = signal(true); + protected isExpandable = signal(false); + + protected readonly requiredRoles = [Role.AlertListWrite]; alertLevelColor: AlertLevelColor | undefined; icon: string; @@ -80,6 +91,15 @@ export class AlertComponent implements OnChanges { this.setStyles(); } + ngAfterViewInit(): void { + const alertMessageElement = this.alertMessage.nativeElement; + this.isExpandable.set(alertMessageElement.scrollHeight > alertMessageElement.offsetHeight); + } + + toggleCollapse(): void { + this.isCollapsed.set(!this.isCollapsed()); + } + onDismiss(): void { this.store$.dispatch(dismissAlertPressed({ id: this.alert().id })); } diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json index 8f14b1a4188..38cdad413ef 100644 --- a/src/assets/i18n/uk.json +++ b/src/assets/i18n/uk.json @@ -4966,8 +4966,8 @@ "Title": "Заголовок", "To activate this periodic snapshot schedule, set this option. To disable this task without deleting it, unset this option.": "Щоб активувати цей періодичний розклад знімків, установіть цей параметр. Щоб вимкнути це завдання, не видаляючи його, зніміть цей параметр.", "To configure Isolated GPU Device(s), click the \"Configure\" button.": "Щоб налаштувати ізольований пристрій (пристрої GPU), натисніть кнопку \"Налаштувати\".", - "Toggle Collapse": "Переключити Стиснення", - "Toggle {row}": "Перемикач {row}", + "Toggle Collapse": "Переключити Згортання", + "Toggle {row}": "Переключити {row}", "Token": "Токен", "Token created with Google Drive.": "Токен створений за допомогою Диска Google.", "Token created with Google Drive. Access Tokens expire periodically and must be refreshed.": "Токен, створений за допомогою Google Диска. Токени доступу періодично закінчуються і повинні оновлюватися.",