feat: add bulk triage view component and related stories
- Exported BulkTriageViewComponent and its related types from findings module. - Created a new accessibility test suite for score components using axe-core. - Introduced design tokens for score components to standardize styling. - Enhanced score breakdown popover for mobile responsiveness with drag handle. - Added date range selector functionality to score history chart component. - Implemented unit tests for date range selector in score history chart. - Created Storybook stories for bulk triage view and score history chart with date range selector.
This commit is contained in:
260
docs/ui/components/findings-list.md
Normal file
260
docs/ui/components/findings-list.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# FindingsListComponent
|
||||
|
||||
Comprehensive findings list with Evidence-Weighted Score (EWS) integration, filtering, and bulk selection.
|
||||
|
||||
## Overview
|
||||
|
||||
The `FindingsListComponent` displays a sortable, filterable table of vulnerability findings with integrated score loading and display.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<app-findings-list />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `findings` | `Finding[]` | `[]` | Array of findings to display |
|
||||
| `autoLoadScores` | `boolean` | `true` | Auto-fetch scores when findings change |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `findingSelect` | `EventEmitter<ScoredFinding>` | Emits when a finding row is clicked |
|
||||
| `selectionChange` | `EventEmitter<string[]>` | Emits finding IDs when selection changes |
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Finding
|
||||
|
||||
```typescript
|
||||
interface Finding {
|
||||
id: string; // Unique finding ID
|
||||
advisoryId: string; // CVE/GHSA ID
|
||||
packageName: string;
|
||||
packageVersion: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
status: 'open' | 'in_progress' | 'fixed' | 'excepted';
|
||||
publishedAt?: string; // ISO 8601
|
||||
}
|
||||
```
|
||||
|
||||
### ScoredFinding
|
||||
|
||||
```typescript
|
||||
interface ScoredFinding extends Finding {
|
||||
score?: EvidenceWeightedScoreResult;
|
||||
scoreLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Table Columns
|
||||
|
||||
| Column | Description | Sortable |
|
||||
|--------|-------------|----------|
|
||||
| Checkbox | Bulk selection | No |
|
||||
| Score | EWS score pill with flags | Yes |
|
||||
| Advisory | CVE/GHSA identifier | Yes |
|
||||
| Package | Package name and version | Yes |
|
||||
| Severity | CVSS severity level | Yes |
|
||||
| Status | Finding status | Yes |
|
||||
|
||||
## Features
|
||||
|
||||
### Score Loading
|
||||
When `autoLoadScores` is true, scores are fetched automatically via the `SCORING_API` injection token.
|
||||
|
||||
### Bucket Filtering
|
||||
Filter findings by priority bucket using the chip filters:
|
||||
- All (default)
|
||||
- Act Now (90-100)
|
||||
- Schedule Next (70-89)
|
||||
- Investigate (40-69)
|
||||
- Watchlist (0-39)
|
||||
|
||||
### Flag Filtering
|
||||
Filter by active score flags:
|
||||
- Live Signal
|
||||
- Proven Path
|
||||
- Vendor N/A
|
||||
- Speculative
|
||||
|
||||
### Search
|
||||
Text search across advisory ID and package name.
|
||||
|
||||
### Sorting
|
||||
Click column headers to sort. Click again to reverse order.
|
||||
|
||||
### Bulk Selection
|
||||
- Click checkboxes to select individual findings
|
||||
- Use "Select All" to select visible findings
|
||||
- Selection persists across filter changes
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
(findingSelect)="openFinding($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
### Without Auto-Loading Scores
|
||||
|
||||
```html
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
[autoLoadScores]="false"
|
||||
/>
|
||||
```
|
||||
|
||||
### With Selection Handling
|
||||
|
||||
```html
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
[autoLoadScores]="true"
|
||||
(selectionChange)="onSelectionChange($event)"
|
||||
/>
|
||||
|
||||
<div class="bulk-actions" *ngIf="selectedIds.length > 0">
|
||||
<button (click)="acknowledgeSelected()">
|
||||
Acknowledge ({{ selectedIds.length }})
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```typescript
|
||||
selectedIds: string[] = [];
|
||||
|
||||
onSelectionChange(ids: string[]): void {
|
||||
this.selectedIds = ids;
|
||||
}
|
||||
```
|
||||
|
||||
### Full Feature Example
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-vulnerability-dashboard',
|
||||
template: `
|
||||
<div class="dashboard">
|
||||
<h1>Vulnerabilities</h1>
|
||||
|
||||
<app-findings-list
|
||||
[findings]="findings()"
|
||||
[autoLoadScores]="true"
|
||||
(findingSelect)="openFindingDetail($event)"
|
||||
(selectionChange)="updateSelection($event)"
|
||||
/>
|
||||
|
||||
@if (selectedFinding()) {
|
||||
<app-finding-detail-panel
|
||||
[finding]="selectedFinding()"
|
||||
(close)="closeFindingDetail()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class VulnerabilityDashboardComponent {
|
||||
findings = signal<Finding[]>([]);
|
||||
selectedFinding = signal<ScoredFinding | null>(null);
|
||||
selectedIds = signal<string[]>([]);
|
||||
|
||||
constructor(private findingsService: FindingsService) {
|
||||
this.loadFindings();
|
||||
}
|
||||
|
||||
async loadFindings(): Promise<void> {
|
||||
this.findings.set(await this.findingsService.getFindings());
|
||||
}
|
||||
|
||||
openFindingDetail(finding: ScoredFinding): void {
|
||||
this.selectedFinding.set(finding);
|
||||
}
|
||||
|
||||
closeFindingDetail(): void {
|
||||
this.selectedFinding.set(null);
|
||||
}
|
||||
|
||||
updateSelection(ids: string[]): void {
|
||||
this.selectedIds.set(ids);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
The component requires a `SCORING_API` provider:
|
||||
|
||||
```typescript
|
||||
import { SCORING_API, ScoringApiService } from '@app/core/services/scoring.service';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: SCORING_API, useClass: ScoringApiService }
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
### Mock API for Testing
|
||||
|
||||
```typescript
|
||||
import { MockScoringApi } from '@app/core/services/scoring.service';
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: SCORING_API, useClass: MockScoringApi }
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Empty States
|
||||
|
||||
### No Findings
|
||||
|
||||
```html
|
||||
<!-- Displays: "No findings to display" -->
|
||||
<app-findings-list [findings]="[]" />
|
||||
```
|
||||
|
||||
### No Matches
|
||||
|
||||
When filters result in no matches:
|
||||
```html
|
||||
<!-- Displays: "No findings match the current filters" -->
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Proper table semantics with `<table>`, `<thead>`, `<tbody>`
|
||||
- Sortable columns use `aria-sort`
|
||||
- Checkboxes have accessible labels
|
||||
- Filter chips are keyboard navigable
|
||||
- Focus management on filter/sort changes
|
||||
- Screen reader announces result counts
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
app-findings-list {
|
||||
--table-header-bg: #f9fafb;
|
||||
--table-border-color: #e5e7eb;
|
||||
--table-row-hover: #f3f4f6;
|
||||
--table-row-selected: #eff6ff;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [ScorePill](./score-pill.md) - Score display in table
|
||||
- [ScoreBadge](./score-badge.md) - Flag badges in table
|
||||
- [ScoreBreakdownPopover](./score-breakdown-popover.md) - Score details on click
|
||||
- [BulkTriageView](./bulk-triage-view.md) - Bulk operations
|
||||
Reference in New Issue
Block a user