Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
@if (isBulletin(item)) {
@if (asBulletin(item); as bulletin) {
@if (bulletin.canRead) {
<li>
<li class="w-full">
<div class="inline-flex flex-wrap gap-x-1.5">
<div>{{ bulletin.timestamp }}</div>
<div class="font-bold {{ getSeverity(bulletin.bulletin.level) }}">
Expand All @@ -64,8 +64,23 @@
@if (!!bulletin.nodeAddress) {
<div>{{ bulletin.nodeAddress }}</div>
}
<pre class="whitespace-pre-wrap">{{ bulletin.bulletin.message }}</pre>
<pre class="whitespace-pre-wrap" [copy]="getBulletinCopyMessage(bulletin)">{{
bulletin.bulletin.message
}}</pre>
</div>
@if (bulletin.bulletin.stackTrace) {
<div class="w-full">
<a class="italic" (click)="toggleStackTrace(bulletin)">
{{ isExpanded(bulletin) ? 'Hide stack trace' : 'Show stack trace' }}
</a>
@if (isExpanded(bulletin)) {
<pre
class="stack-trace mt-2 p-2 border whitespace-pre-wrap overflow-auto max-h-96"
>{{ bulletin.bulletin.stackTrace }}</pre
>
}
</div>
}
</li>
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.stack-trace {
background-color: var(--mat-sys-surface-container);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
* limitations under the License.
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { BulletinBoardList } from './bulletin-board-list.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
Expand All @@ -27,7 +28,7 @@ describe('BulletinBoardList', () => {
beforeEach(() => {
Element.prototype.scroll = jest.fn();
TestBed.configureTestingModule({
imports: [BulletinBoardList, NoopAnimationsModule]
imports: [BulletinBoardList, NoopAnimationsModule, RouterTestingModule]
});
fixture = TestBed.createComponent(BulletinBoardList);
component = fixture.componentInstance;
Expand All @@ -37,4 +38,234 @@ describe('BulletinBoardList', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should emit filterChanged when applyFilter is called', () => {
const received: any[] = [];
component.filterChanged.subscribe((args) => received.push(args));
component.applyFilter('foo', 'message');
expect(received).toEqual([{ filterTerm: 'foo', filterColumn: 'message' }]);
});

it('should debounce filter term changes before emitting', fakeAsync(() => {
const received: any[] = [];
component.filterChanged.subscribe((args) => received.push(args));

component.filterForm.get('filterTerm')!.setValue('abc');
tick(499);
expect(received.length).toBe(0);
tick(1);
expect(received).toEqual([{ filterTerm: 'abc', filterColumn: 'message' }]);
}));

it('should emit immediately when filter column changes', () => {
const received: any[] = [];
component.filterChanged.subscribe((args) => received.push(args));
component.filterForm.get('filterColumn')!.setValue('name');
expect(received).toEqual([{ filterTerm: '', filterColumn: 'name' }]);
});

it('should return proper severity classes', () => {
expect(component.getSeverity('error')).toBe('bulletin-error error-color');
expect(component.getSeverity('warn')).toBe('bulletin-warn caution-color');
expect(component.getSeverity('warning')).toBe('bulletin-warn caution-color');
expect(component.getSeverity('info')).toBe('bulletin-normal success-color-default');
});

it('should determine bulletin vs event types correctly', () => {
const bulletin = {
canRead: true,
id: 1,
sourceId: 's1',
groupId: 'g1',
timestamp: 'now',
bulletin: {
id: 1,
sourceId: 's1',
groupId: 'g1',
category: 'cat',
level: 'INFO',
message: 'm',
sourceName: 'sn',
timestamp: 'now',
sourceType: 'PROCESSOR'
}
} as any;

const event = { type: 'filter', message: 'applied' } as any;

const bulletinItem = { item: bulletin } as any;
const eventItem = { item: event } as any;

expect(component.isBulletin(bulletinItem)).toBe(true);
expect(component.asBulletin(bulletinItem)).toBe(bulletin);
expect(component.asBulletinEvent(bulletinItem)).toBeNull();

expect(component.isBulletin(eventItem)).toBe(false);
expect(component.asBulletin(eventItem)).toBeNull();
expect(component.asBulletinEvent(eventItem)).toBe(event);
});

it('should build router links for known component types and contexts', () => {
const base = {
canRead: true,
id: 1,
sourceId: 'sid',
groupId: 'gid',
timestamp: 't'
} as any;

const generateBulletin = (sourceType: string, groupId: string | null = 'gid') =>
({
...base,
groupId: groupId ?? (undefined as any),
bulletin: {
id: 1,
sourceId: 'sid',
groupId: groupId ?? undefined,
category: 'c',
level: 'INFO',
message: 'm',
sourceName: 'sn',
timestamp: 't',
sourceType
}
}) as any;

expect(component.getRouterLink(generateBulletin('CONTROLLER_SERVICE'))).toEqual([
'/process-groups',
'gid',
'controller-services',
'sid'
]);
expect(component.getRouterLink(generateBulletin('CONTROLLER_SERVICE', null))).toEqual([
'/settings',
'management-controller-services',
'sid'
]);
expect(component.getRouterLink(generateBulletin('REPORTING_TASK'))).toEqual([
'/settings',
'reporting-tasks',
'sid'
]);
expect(component.getRouterLink(generateBulletin('FLOW_REGISTRY_CLIENT'))).toEqual([
'/settings',
'registry-clients',
'sid'
]);
expect(component.getRouterLink(generateBulletin('FLOW_ANALYSIS_RULE'))).toEqual([
'/settings',
'flow-analysis-rules',
'sid'
]);
expect(component.getRouterLink(generateBulletin('PARAMETER_PROVIDER'))).toEqual([
'/settings',
'parameter-providers',
'sid'
]);
expect(component.getRouterLink(generateBulletin('PROCESSOR'))).toEqual([
'/process-groups',
'gid',
'Processor',
'sid'
]);
expect(component.getRouterLink(generateBulletin('UNKNOWN'))).toBeNull();
});

it('should toggle stack trace expansion by bulletin id', () => {
const bulletin = {
canRead: true,
id: 1,
sourceId: 's1',
groupId: 'g1',
timestamp: 'now',
bulletin: {
id: 42,
sourceId: 's1',
groupId: 'g1',
category: 'cat',
level: 'ERROR',
message: 'm',
stackTrace: 'st',
sourceName: 'sn',
timestamp: 'now',
sourceType: 'PROCESSOR'
}
} as any;

expect(component.isExpanded(bulletin)).toBe(false);
component.toggleStackTrace(bulletin);
expect(component.isExpanded(bulletin)).toBe(true);
component.toggleStackTrace(bulletin);
expect(component.isExpanded(bulletin)).toBe(false);
});

it('should compose bulletin copy message with optional stack trace', () => {
const base = {
canRead: true,
id: 1,
sourceId: 's1',
groupId: 'g1',
timestamp: 'now'
} as any;
const withStack = {
...base,
bulletin: {
id: 1,
sourceId: 's1',
groupId: 'g1',
category: 'c',
level: 'ERROR',
message: 'm',
stackTrace: 'st',
sourceName: 'sn',
timestamp: 'now',
sourceType: 'PROCESSOR'
}
} as any;
const withoutStack = {
...base,
bulletin: {
id: 1,
sourceId: 's1',
groupId: 'g1',
category: 'c',
level: 'ERROR',
message: 'm',
sourceName: 'sn',
timestamp: 'now',
sourceType: 'PROCESSOR'
}
} as any;

expect(component.getBulletinCopyMessage(withStack)).toBe('m\n\nst');
expect(component.getBulletinCopyMessage(withoutStack)).toBe('m');
});

it('should auto-scroll when bulletins change', fakeAsync(() => {
const scrollSpy = jest.spyOn(Element.prototype as any, 'scroll');
scrollSpy.mockClear();
const bulletin = {
canRead: true,
id: 1,
sourceId: 's1',
groupId: 'g1',
timestamp: 'now',
bulletin: {
id: 1,
sourceId: 's1',
groupId: 'g1',
category: 'c',
level: 'INFO',
message: 'm',
sourceName: 'sn',
timestamp: 'now',
sourceType: 'PROCESSOR'
}
} as any;

component.bulletinBoardItems = [{ item: bulletin } as any];
fixture.detectChanges();
tick(11);
expect(scrollSpy).toHaveBeenCalledTimes(1);
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,24 @@ import { MatInputModule } from '@angular/material/input';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { BulletinEntity, ComponentType, NiFiCommon } from '@nifi/shared';
import { BulletinEntity, ComponentType, CopyDirective, NiFiCommon } from '@nifi/shared';
import { BulletinBoardEvent, BulletinBoardFilterArgs, BulletinBoardItem } from '../../../state/bulletin-board';
import { debounceTime, delay, Subject } from 'rxjs';
import { RouterLink } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
selector: 'bulletin-board-list',
imports: [MatFormFieldModule, MatInputModule, MatOptionModule, MatSelectModule, ReactiveFormsModule, RouterLink],
standalone: true,
imports: [
MatFormFieldModule,
MatInputModule,
MatOptionModule,
MatSelectModule,
ReactiveFormsModule,
RouterLink,
CopyDirective
],
templateUrl: './bulletin-board-list.component.html',
styleUrls: ['./bulletin-board-list.component.scss']
})
Expand All @@ -59,6 +68,8 @@ export class BulletinBoardList implements AfterViewInit, OnDestroy {

@ViewChild('scrollContainer') private scroll!: ElementRef;

expandedBulletinIds: Set<number> = new Set<number>();

@Input() set bulletinBoardItems(items: BulletinBoardItem[]) {
this._items = items;
this.bulletinsChanged$.next();
Expand Down Expand Up @@ -216,4 +227,23 @@ export class BulletinBoardList implements AfterViewInit, OnDestroy {
return null;
}
}

isExpanded(bulletin: BulletinEntity): boolean {
return this.expandedBulletinIds.has(bulletin.bulletin.id);
}

toggleStackTrace(bulletin: BulletinEntity): void {
const id = bulletin.bulletin.id;
if (this.expandedBulletinIds.has(id)) {
this.expandedBulletinIds.delete(id);
} else {
this.expandedBulletinIds.add(id);
}
}
getBulletinCopyMessage(bulletin: BulletinEntity): string {
if (bulletin.bulletin.stackTrace) {
return bulletin.bulletin.message + '\n\n' + bulletin.bulletin.stackTrace;
}
return bulletin.bulletin.message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@if (bulletinEntity.canRead) {
<li>
<div class="inline-flex flex-wrap gap-x-1.5">
<div class="inline-flex flex-wrap gap-x-1.5" [copy]="bulletinEntity.bulletin.message">
<div class="inline-flex flex-wrap gap-x-1.5">
<div>{{ bulletinEntity.bulletin.timestamp }}</div>
@if (bulletinEntity.nodeAddress) {
<div>{{ bulletinEntity.nodeAddress }}</div>
Expand All @@ -30,7 +30,9 @@
{{ bulletinEntity.bulletin.level }}
</div>
</div>
<pre class="whitespace-pre-wrap">{{ bulletinEntity.bulletin.message }}</pre>
<pre class="whitespace-pre-wrap" [copy]="getBulletinCopyMessage(bulletinEntity)">{{
bulletinEntity.bulletin.message
}}</pre>
</div>
</li>
}
Expand Down
Loading