-import { Component, DestroyRef, OnInit, SecurityContext, ViewChild } from '@angular/core';
+import { Component, DestroyRef, OnInit, ViewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { NotificationService } from '~/app/shared/services/notification.service';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { catchError, concatMap, map, tap } from 'rxjs/operators';
-import { DomSanitizer } from '@angular/platform-browser';
export type SubsystemPayload = {
nqn: string;
private destroyRef: DestroyRef,
private nvmeofService: NvmeofService,
private notificationService: NotificationService,
- private router: Router,
- private sanitizer: DomSanitizer
+ private router: Router
) {}
ngOnInit() {
private showFinalNotification(stepResults: StepResult[]) {
this.isSubmitLoading = false;
- const messageLines = stepResults.map((stepResult) =>
- stepResult.success
- ? $localize`<div>${stepResult.step} step created successfully</div><br/>`
- : $localize`<div>${stepResult.step} step failed: <code>${stepResult.error}</code></div><br/>`
- );
-
- const rawHtml = messageLines.join('<br/>');
- const sanitizedHtml = this.sanitizer.sanitize(SecurityContext.HTML, rawHtml) ?? '';
-
const hasFailure = stepResults.some((r) => !r.success);
const type = hasFailure ? NotificationType.error : NotificationType.success;
const title = hasFailure
? $localize`Subsystem created (with errors)`
: $localize`Subsystem created`;
- this.notificationService.show(type, title, sanitizedHtml);
+ const messageLines = stepResults.map((stepResult) =>
+ stepResult.success
+ ? $localize`${stepResult.step}:step: created successfully`
+ : $localize`${stepResult.step}:step: failed (${stepResult.error}:error:)`
+ );
+
+ this.notificationService.show(type, title, messageLines.join('<br>'));
this.router.navigate(['block/nvmeof/subsystems'], {
queryParams: {
group: this.group,
<cd-icon type="infoCircle"></cd-icon>
<div class="notification-content">
- <div class="notification-title">
+ <div class="notification-title cds--type-body-short-01">
{{ task.description }}
</div>
</cds-progress-bar>
<div class="task-row">
- <span class="notification-timestamp">
+ <span class="notification-timestamp cds--type-label-01">
{{ task.begin_time | relativeDate }}
</span>
Occurrences: {{ selected.occurrences }}
</p>
}
- <p class="notifications-page__detail-text cds--type-body-01">
- {{ selected.displayPreview }}
- </p>
+ <p
+ class="notifications-page__detail-text cds--type-body-01"
+ [innerHTML]="selected.displayDetail | sanitizeHtml"
+ ></p>
@if (selected.prometheusAlert?.sourceUrl) {
<a
cdsLink
- class="cds-mt-5"
+ class="notifications-page__prometheus-link cds-mt-5"
[href]="selected.prometheusAlert.sourceUrl"
target="_blank"
rel="noopener noreferrer"
- i18n
>
- View in Prometheus
+ <span i18n>View in Prometheus</span>
+ <cd-icon
+ type="launch"
+ [useDefault]="true"
+ ></cd-icon>
</a>
}
</div>
&__detail {
overflow-y: auto;
+ overflow-x: hidden;
padding: var(--cds-spacing-06) var(--cds-spacing-07);
background-color: var(--cds-layer-01);
+ min-width: 0;
}
&__detail-header cd-notification-item {
&__detail-text {
margin: 0;
color: var(--cds-text-primary);
+ overflow-wrap: break-word;
+ }
+
+ &__prometheus-link {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--cds-spacing-02);
}
&__empty {
fixture.detectChanges();
expect(component.notifications()[0].displayPreview).toBe('');
});
+
+ it('should strip HTML tags from displayPreview', () => {
+ const htmlNotification = createMockNotification({
+ id: '6',
+ message: 'Disk full. <a href="http://example.com">View details</a>'
+ });
+ dataSourceSubject.next([htmlNotification]);
+ fixture.detectChanges();
+ expect(component.notifications()[0].displayPreview).toBe('Disk full. View details');
+ });
+
+ it('should preserve HTML in displayDetail', () => {
+ const htmlNotification = createMockNotification({
+ id: '6',
+ message: 'Disk full. <a href="http://example.com">View details</a>'
+ });
+ dataSourceSubject.next([htmlNotification]);
+ fixture.detectChanges();
+ expect(component.notifications()[0].displayDetail).toBe(
+ 'Disk full. <a href="http://example.com">View details</a>'
+ );
+ });
});
describe('query param pre-selection', () => {
interface DisplayNotification extends CdNotification {
displayTitle: string;
displayPreview: string;
+ displayDetail: string;
}
@Component({
this.sub = this.notificationService.data$.subscribe((notifications) => {
this.notifications.set(
- notifications.map((n) =>
- Object.assign(n, {
+ notifications.map((n) => {
+ const rawMessage = n.prometheusAlert?.description || n.message || '';
+ return Object.assign(n, {
displayTitle: n.prometheusAlert?.alertName || n.title || '',
- displayPreview: n.prometheusAlert?.description || n.message || ''
- })
- )
+ displayPreview: rawMessage.replace(/<[^>]*>/g, ''),
+ displayDetail: rawMessage
+ });
+ })
);
this._tryPreselect(notifications);
justify-content: flex-start;
align-items: center;
width: 100%;
+ gap: $spacing-03;
}
.toast-caption-container .date {
> div {
flex: 1;
+ min-height: 0;
+ min-block-size: 0;
}
}
return this.notificationService.show(() => {
let message = '';
if (_.isPlainObject(resp.error) && _.isString(resp.error.detail)) {
- message = resp.error.detail; // Error was triggered by the backend.
+ message = resp.error.detail;
} else if (_.isString(resp.error)) {
message = resp.error;
} else if (_.isString(resp.message)) {
message = resp.message;
}
+
return new CdNotificationConfig(
NotificationType.error,
`${resp.status} - ${resp.statusText}`,
.replace(/'/g, ''');
}
+function toPlainText(html: string): string {
+ return html
+ .replace(/<[^>]*>/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+}
+
@Injectable({
providedIn: 'root'
})
const lowContrast = notification.options?.lowContrast || false;
const escapedTitle = escapeHtml(notification.title || '');
- const escapedMessage = escapeHtml(notification.message || '');
+ const escapedMessage = escapeHtml(toPlainText(notification.message || ''));
const existing = this.activeToasts.find(
(t) =>
t.title === escapedTitle && t.type === carbonType && t.originalSubtitle === escapedMessage