diff --git a/docs/features/checked/web/domain-signal-chips-adoption.md b/docs/features/checked/web/domain-signal-chips-adoption.md new file mode 100644 index 000000000..829af9f94 --- /dev/null +++ b/docs/features/checked/web/domain-signal-chips-adoption.md @@ -0,0 +1,49 @@ +# Domain Signal Chips Adoption (DigestChip + ReachabilityStateChip) + +## Module +Web + +## Status +VERIFIED + +## Description +Replaced hand-rolled digest truncation/copy markup and bespoke reachability state +display with shared domain chip components (`DigestChipComponent`, +`ReachabilityStateChipComponent`) across mounted (routed) consumer surfaces. + +## Implementation Details + +### DigestChipComponent adoption (4 consumers) +- `src/Web/StellaOps.Web/src/app/features/releases/releases-list-page.component.ts` + - Replaced `shortDigest()` + `copyDigest()` + inline SVG copy button with `` +- `src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts` + - Replaced `shortDigest()` computed + `copyDigest()` snackbar-based copy with `` +- `src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts` + - Replaced `shortDigest()` method with `` +- `src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts` + - Replaced `truncateDigest()` + `copyDigest()` + clipboard icon with `` + +### ReachabilityStateChipComponent adoption (1 consumer) +- `src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts` + - Added `` to witness rows, mapping `isReachable` boolean to `ReachabilityState` type and `confidenceScore` to confidence input + - Added `reachabilityState()` helper method for type mapping + +### Shared chip source components +- `src/Web/StellaOps.Web/src/app/shared/domain/digest-chip/digest-chip.component.ts` +- `src/Web/StellaOps.Web/src/app/shared/domain/reachability-state-chip/reachability-state-chip.component.ts` + +## Focused tests +- `src/Web/StellaOps.Web/src/app/features/releases/releases-list-page.component.spec.ts` +- `src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.spec.ts` +- `src/Web/StellaOps.Web/src/app/features/reachability/reachability-center-chip-adoption.component.spec.ts` +- Updated: `src/Web/StellaOps.Web/src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts` + +## Exclusions +- `finding-list` and `finding-row` consumers reserved for sprint 020 +- Dead witness pages and disconnected route files excluded +- Only mounted (currently routed) surfaces adopted + +## Verification +- Date: 2026-03-08 +- Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption +- Route verification: all consumers confirmed reachable via route tree diff --git a/docs/implplan/SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption.md b/docs/implplan/SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption.md new file mode 100644 index 000000000..1f4b02941 --- /dev/null +++ b/docs/implplan/SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption.md @@ -0,0 +1,91 @@ +# Sprint 20260308_013 - FE Orphan Domain Signal Chips Adoption + +## Topic & Scope +- Replace hand-rolled digest truncation/copy markup with `DigestChipComponent` in mounted consumers. +- Replace bespoke reachability state text/badges with `ReachabilityStateChipComponent` in mounted consumers. +- Working directory: `src/Web/StellaOps.Web`. +- Expected evidence: updated component files, focused Angular tests, checked-feature note. + +## Dependencies & Concurrency +- No upstream sprint dependencies. +- Do NOT touch `finding-list` or `finding-row` consumers (reserved for sprint 020). +- Do NOT reopen dead witness pages or reconnect route files. + +## Documentation Prerequisites +- `src/Web/StellaOps.Web/src/app/shared/domain/digest-chip/digest-chip.component.ts` +- `src/Web/StellaOps.Web/src/app/shared/domain/reachability-state-chip/reachability-state-chip.component.ts` + +## Delivery Tracker + +### FE-ODSC-001 - Freeze consumer list +Status: DONE +Dependency: none +Owners: Developer (FE) +Task description: +- Read the source chip components. +- Search `features/` for mounted consumers that hand-roll digest truncation/copy or reachability state display. +- Verify each consumer is reachable from the current route tree. +- Record the frozen list in the execution log. + +Completion criteria: +- [x] Frozen list recorded in Execution Log +- [x] Each consumer verified as mounted via route tree + +### FE-ODSC-002 - Adopt DigestChipComponent +Status: DONE +Dependency: FE-ODSC-001 +Owners: Developer (FE) +Task description: +- In the frozen consumer list, replace ad-hoc digest truncation and copy markup with `DigestChipComponent`. +- Import it in each consumer's imports array and use `` in templates. +- Preserve verification labels. + +Completion criteria: +- [x] DigestChipComponent imported and used in each frozen digest consumer +- [x] Hand-rolled truncation/copy methods removed from adopted consumers +- [x] Existing verification labels preserved + +### FE-ODSC-003 - Adopt ReachabilityStateChipComponent +Status: DONE +Dependency: FE-ODSC-001 +Owners: Developer (FE) +Task description: +- In the frozen reachability consumer list, replace bespoke reachability text/badges with `ReachabilityStateChipComponent`. +- Import and use `` in templates. +- Map existing data into the chip's state/confidence inputs. + +Completion criteria: +- [x] ReachabilityStateChipComponent imported and used in each frozen reachability consumer +- [x] Bespoke reachability display logic retained for backward compat (reachabilityLabel kept as it is used in filtering) +- [x] Data mapped correctly into state/confidence inputs via reachabilityState() helper + +### FE-ODSC-004 - Verify and document +Status: DONE +Dependency: FE-ODSC-002, FE-ODSC-003 +Owners: Developer (FE) +Task description: +- Add focused Angular tests for the adopted consumers. +- Create a checked-feature note under `docs/features/checked/web/`. +- Update the sprint execution log with results. + +Completion criteria: +- [x] Focused Angular tests added for adopted consumers +- [x] Checked-feature note created at `docs/features/checked/web/domain-signal-chips-adoption.md` +- [x] Sprint execution log updated with results + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-08 | Sprint created; FE-ODSC-001 DOING. | Developer (FE) | +| 2026-03-08 | FE-ODSC-001 DONE. Frozen consumer list: **DigestChip**: (1) `releases-list-page.component.ts` (route: `releases.routes.ts`, has `shortDigest()`+`copyDigest()`), (2) `evidence-thread-view.component.ts/html` (route: `evidence-thread.routes.ts`, has `shortDigest()`+`copyDigest()`), (3) `evidence-thread-list.component.ts/html` (route: `evidence-thread.routes.ts`, has `shortDigest()`), (4) `attestation-links.component.ts` (child of lineage, routed via `lineage.routes.ts`, has `truncateDigest()`+`copyDigest()`). **ReachabilityStateChip**: (1) `reachability-center.component.ts/html` (route: `security.routes.ts` + `security-risk.routes.ts`, has `reachabilityLabel()`+`confidenceLabel()`). All verified as mounted. | Developer (FE) | +| 2026-03-08 | FE-ODSC-002 DONE. DigestChipComponent adopted in all 4 frozen consumers. Removed 4 hand-rolled `shortDigest`/`truncateDigest` methods and 3 `copyDigest` methods. Imported DigestChipComponent in each consumer's imports array. | Developer (FE) | +| 2026-03-08 | FE-ODSC-003 DONE. ReachabilityStateChipComponent adopted in reachability-center. Added `reachabilityState()` helper to map `isReachable` boolean to `ReachabilityState` type. Retained `reachabilityLabel()` and `confidenceLabel()` for backward compat with filter logic. | Developer (FE) | +| 2026-03-08 | FE-ODSC-004 DONE. Created 3 focused test files: releases-list-page.component.spec.ts (6 tests), attestation-links.component.spec.ts (7 tests), reachability-center-chip-adoption.component.spec.ts (6 tests). Updated existing evidence-thread-view spec to remove shortDigest reference. Created checked-feature note at `docs/features/checked/web/domain-signal-chips-adoption.md`. | Developer (FE) | + +## Decisions & Risks +- Scope limited to mounted (currently routed) surfaces only. +- finding-list/finding-row excluded per sprint 020 reservation. +- Dead witness pages and disconnected route files excluded. + +## Next Checkpoints +- All tasks DONE by end of sprint. diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts index 2f71c072d..72656e3ae 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts @@ -144,10 +144,9 @@ describe('EvidenceThreadViewComponent', () => { expect(component.getVerdictIcon('unknown')).toBe('help_outline'); }); - it('should compute short digest correctly', () => { + it('should set artifact digest for the digest chip', () => { fixture.detectChanges(); - const shortDigest = component.shortDigest(); - expect(shortDigest).toBe('sha256:abc123...'); + expect(component.artifactDigest()).toBe('sha256:abc123'); }); it('should compute node count correctly', () => { diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.html b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.html index e13f60aa5..fc9b1d638 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.html +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.html @@ -92,7 +92,10 @@
{{ thread.artifactName ?? 'Unnamed' }} - {{ shortDigest(thread.artifactDigest) }} +
diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts index b15e88d6f..798288e27 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts @@ -26,6 +26,7 @@ import { EvidenceVerdict, EvidenceThreadFilter } from '../../services/evidence-thread.service'; +import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component'; @Component({ selector: 'stella-evidence-thread-list', @@ -44,7 +45,8 @@ import { MatChipsModule, MatProgressSpinnerModule, MatTooltipModule, - MatCardModule + MatCardModule, + DigestChipComponent ], templateUrl: './evidence-thread-list.component.html', styleUrls: ['./evidence-thread-list.component.scss'], @@ -194,10 +196,6 @@ export class EvidenceThreadListComponent implements OnInit { }); } - shortDigest(digest: string): string { - return digest.length > 19 ? `${digest.substring(0, 19)}...` : digest; - } - getRiskClass(riskScore: number): string { if (riskScore >= 7) return 'risk-critical'; if (riskScore >= 4) return 'risk-high'; diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.html b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.html index 6adcda29b..aa5c3ebfe 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.html +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.html @@ -16,10 +16,10 @@ }
- {{ shortDigest() }} - +
diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts index 37709fadc..8406416d8 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts @@ -23,6 +23,7 @@ import { EvidenceTranscriptPanelComponent } from '../evidence-transcript-panel/e import { EvidenceNodeCardComponent } from '../evidence-node-card/evidence-node-card.component'; import { EvidenceExportDialogComponent } from '../evidence-export-dialog/evidence-export-dialog.component'; import { TranslatePipe } from '../../../../core/i18n/translate.pipe'; +import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component'; @Component({ selector: 'stella-evidence-thread-view', @@ -42,7 +43,8 @@ import { TranslatePipe } from '../../../../core/i18n/translate.pipe'; EvidenceTimelinePanelComponent, EvidenceTranscriptPanelComponent, EvidenceNodeCardComponent, - TranslatePipe + TranslatePipe, + DigestChipComponent ], templateUrl: './evidence-thread-view.component.html', styleUrls: ['./evidence-thread-view.component.scss'], @@ -91,13 +93,6 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy { readonly nodeCount = computed(() => this.nodes().length); readonly linkCount = computed(() => this.links().length); - readonly shortDigest = computed(() => { - const digest = this.artifactDigest(); - if (!digest) return ''; - // Show first 12 chars of digest (sha256:xxxx...) - return digest.length > 19 ? `${digest.substring(0, 19)}...` : digest; - }); - ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe(params => { const digest = params['artifactDigest']; @@ -182,23 +177,4 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy { return icons[verdict ?? 'unknown'] ?? 'help_outline'; } - copyDigest(): void { - const digest = this.artifactDigest(); - if (!digest || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) { - this.snackBar.open('Clipboard not available', 'Dismiss', { - duration: 3000 - }); - return; - } - - navigator.clipboard.writeText(digest).then(() => { - this.snackBar.open('Digest copied to clipboard', 'Dismiss', { - duration: 2000 - }); - }).catch(() => { - this.snackBar.open('Failed to copy digest', 'Dismiss', { - duration: 3000 - }); - }); - } } diff --git a/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.spec.ts new file mode 100644 index 000000000..d4bece274 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.spec.ts @@ -0,0 +1,86 @@ +/** + * Unit tests for AttestationLinksComponent -- DigestChipComponent adoption. + * Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004) + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { AttestationLinksComponent } from './attestation-links.component'; +import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component'; + +describe('AttestationLinksComponent - DigestChip adoption', () => { + let component: AttestationLinksComponent; + let fixture: ComponentFixture; + + const mockAttestations = [ + { + digest: 'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + predicateType: 'https://slsa.dev/provenance/v1', + createdAt: '2026-01-15T10:30:00Z', + rekorIndex: 12345, + rekorLogId: 'abc123', + viewUrl: 'https://example.com/att/1' + }, + { + digest: 'sha256:deadbeef0000111122223333444455556666777788889999aaaabbbbccccddd0', + predicateType: 'https://in-toto.io/Statement/v1', + createdAt: '2026-01-16T14:00:00Z', + } + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AttestationLinksComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(AttestationLinksComponent); + component = fixture.componentInstance; + component.attestations = mockAttestations; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should render one DigestChipComponent per attestation', () => { + const digestChips = fixture.debugElement.queryAll( + By.directive(DigestChipComponent) + ); + expect(digestChips.length).toBe(mockAttestations.length); + }); + + it('should pass attestation digest to each chip', () => { + const digestChips = fixture.debugElement.queryAll( + By.directive(DigestChipComponent) + ); + digestChips.forEach((chip, index) => { + const chipInstance = chip.componentInstance as DigestChipComponent; + expect(chipInstance.digest).toBe(mockAttestations[index].digest); + }); + }); + + it('should use artifact variant for digest chips', () => { + const digestChips = fixture.debugElement.queryAll( + By.directive(DigestChipComponent) + ); + for (const chip of digestChips) { + const chipInstance = chip.componentInstance as DigestChipComponent; + expect(chipInstance.variant).toBe('artifact'); + } + }); + + it('should not have hand-rolled truncateDigest method', () => { + expect((component as any).truncateDigest).toBeUndefined(); + }); + + it('should not have hand-rolled copyDigest method', () => { + expect((component as any).copyDigest).toBeUndefined(); + }); + + it('should still render Rekor links when available', () => { + const rekorLinks = fixture.debugElement.queryAll(By.css('.rekor-link')); + expect(rekorLinks.length).toBeGreaterThan(0); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts b/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts index 107bee554..6d5514766 100644 --- a/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts @@ -8,17 +8,17 @@ import { Component, Input } from '@angular/core'; import { AttestationLink } from '../../models/lineage.models'; import { - ICON_CLIPBOARD, ICON_EXTERNAL_LINK, ICON_CHECK, } from '../../icons/lineage-icons'; +import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component'; /** * Attestation links component showing signed attestations with Rekor links. */ @Component({ selector: 'app-attestation-links', - imports: [], + imports: [DigestChipComponent], template: `