feat(ui): adopt domain signal chips on mounted surfaces [SPRINT-013]

Replace hand-rolled digest truncation/copy and reachability badges with
shared DigestChipComponent and ReachabilityStateChipComponent on releases
list, evidence-thread, attestation-links, and reachability-center.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
master
2026-03-08 19:24:39 +02:00
parent f24d49ddeb
commit c52ca82652
14 changed files with 395 additions and 90 deletions

View File

@@ -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 `<app-digest-chip variant="bundle">`
- `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 `<app-digest-chip variant="artifact">`
- `src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts`
- Replaced `shortDigest()` method with `<app-digest-chip variant="artifact">`
- `src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts`
- Replaced `truncateDigest()` + `copyDigest()` + clipboard icon with `<app-digest-chip variant="artifact">`
### ReachabilityStateChipComponent adoption (1 consumer)
- `src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts`
- Added `<app-reachability-state-chip>` 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

View File

@@ -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 `<app-digest-chip>` 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 `<app-reachability-state-chip>` 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.

View File

@@ -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', () => {

View File

@@ -92,7 +92,10 @@
<td mat-cell *matCellDef="let thread">
<div class="artifact-cell">
<span class="artifact-name">{{ thread.artifactName ?? 'Unnamed' }}</span>
<code class="artifact-digest">{{ shortDigest(thread.artifactDigest) }}</code>
<app-digest-chip
[digest]="thread.artifactDigest"
variant="artifact"
></app-digest-chip>
</div>
</td>
</ng-container>

View File

@@ -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';

View File

@@ -16,10 +16,10 @@
}
</h1>
<div class="thread-digest">
<code>{{ shortDigest() }}</code>
<button mat-icon-button (click)="copyDigest()" [matTooltip]="'ui.evidence_thread.copy_digest' | translate" class="copy-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
<app-digest-chip
[digest]="artifactDigest()"
variant="artifact"
></app-digest-chip>
</div>
</div>
</div>

View File

@@ -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
});
});
}
}

View File

@@ -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<AttestationLinksComponent>;
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);
});
});

View File

@@ -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: `
<div class="attestation-links">
@if (attestations.length === 0) {
@@ -35,10 +35,10 @@ import {
<div class="card-body">
<div class="digest-row">
<span class="label">Digest:</span>
<code class="digest">{{ truncateDigest(att.digest) }}</code>
<button class="copy-btn" (click)="copyDigest(att.digest)" title="Copy full digest">
<span [innerHTML]="clipboardIcon"></span>
</button>
<app-digest-chip
[digest]="att.digest"
variant="artifact"
></app-digest-chip>
</div>
@if (att.rekorIndex !== undefined) {
@@ -195,7 +195,6 @@ import {
`]
})
export class AttestationLinksComponent {
readonly clipboardIcon = ICON_CLIPBOARD;
readonly externalLinkIcon = ICON_EXTERNAL_LINK;
readonly checkIcon = ICON_CHECK;
@@ -220,15 +219,6 @@ export class AttestationLinksComponent {
}
}
truncateDigest(digest: string): string {
if (!digest) return '';
const colonIndex = digest.indexOf(':');
if (colonIndex >= 0 && digest.length > colonIndex + 16) {
return `${digest.substring(0, colonIndex + 17)}...`;
}
return digest.length > 20 ? `${digest.substring(0, 20)}...` : digest;
}
getRekorUrl(att: AttestationLink): string {
if (att.rekorLogId) {
return `https://search.sigstore.dev/?logIndex=${att.rekorIndex}`;
@@ -239,11 +229,4 @@ export class AttestationLinksComponent {
return '#';
}
async copyDigest(digest: string): Promise<void> {
try {
await navigator.clipboard.writeText(digest);
} catch (err) {
console.error('Failed to copy digest:', err);
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* Focused tests for ReachabilityCenterComponent -- ReachabilityStateChip adoption.
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReachabilityCenterComponent } from './reachability-center.component';
import type { ReachabilityWitness } from '../../core/api/witness.models';
describe('ReachabilityCenterComponent - ReachabilityStateChip adoption', () => {
let fixture: ComponentFixture<ReachabilityCenterComponent>;
let component: ReachabilityCenterComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReachabilityCenterComponent],
}).compileComponents();
fixture = TestBed.createComponent(ReachabilityCenterComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should map reachable witness to Reachable state', () => {
const witness = { isReachable: true, confidenceScore: 0.85 } as ReachabilityWitness;
expect(component.reachabilityState(witness)).toBe('Reachable');
});
it('should map unreachable witness to Unreachable state', () => {
const witness = { isReachable: false, confidenceScore: 0.92 } as ReachabilityWitness;
expect(component.reachabilityState(witness)).toBe('Unreachable');
});
it('should preserve existing reachabilityLabel for backward compat', () => {
const reachableWitness = { isReachable: true, confidenceScore: 0.85 } as ReachabilityWitness;
const unreachableWitness = { isReachable: false, confidenceScore: 0.92 } as ReachabilityWitness;
expect(component.reachabilityLabel(reachableWitness)).toBe('Reachable');
expect(component.reachabilityLabel(unreachableWitness)).toBe('Unreachable');
});
it('should preserve existing confidenceLabel for backward compat', () => {
const witness = { isReachable: true, confidenceScore: 0.856 } as ReachabilityWitness;
expect(component.confidenceLabel(witness)).toBe('86%');
});
it('should round confidence to nearest integer percent', () => {
const witness50 = { isReachable: true, confidenceScore: 0.5 } as ReachabilityWitness;
const witness99 = { isReachable: false, confidenceScore: 0.999 } as ReachabilityWitness;
const witness0 = { isReachable: false, confidenceScore: 0 } as ReachabilityWitness;
expect(component.confidenceLabel(witness50)).toBe('50%');
expect(component.confidenceLabel(witness99)).toBe('100%');
expect(component.confidenceLabel(witness0)).toBe('0%');
});
});

View File

@@ -172,7 +172,10 @@
<tr data-testid="witness-row">
<td>
<strong>{{ witness.witnessId }}</strong>
<span class="subtle">{{ reachabilityLabel(witness) }}</span>
<app-reachability-state-chip
[state]="reachabilityState(witness)"
[confidence]="witness.confidenceScore"
></app-reachability-state-chip>
</td>
<td>{{ witness.cveId ?? witness.vulnId }}</td>
<td>

View File

@@ -28,6 +28,7 @@ import {
readContextRouteState,
} from '../../shared/ui/context-route-state/context-route-state';
import { PoEDrawerComponent } from './poe-drawer.component';
import { ReachabilityStateChipComponent, type ReachabilityState } from '../../shared/domain/reachability-state-chip/reachability-state-chip.component';
import {
type CoverageStatus,
DEFAULT_REACHABILITY_SCAN_ID,
@@ -71,6 +72,7 @@ const TIER_FILTERS: readonly TierFilter[] = [
PoEDrawerComponent,
ContextHeaderComponent,
TabbedNavComponent,
ReachabilityStateChipComponent,
],
templateUrl: './reachability-center.component.html',
styleUrls: ['./reachability-center.component.scss'],
@@ -358,6 +360,10 @@ export class ReachabilityCenterComponent implements OnInit {
return witness.isReachable ? 'Reachable' : 'Unreachable';
}
reachabilityState(witness: ReachabilityWitness): ReachabilityState {
return witness.isReachable ? 'Reachable' : 'Unreachable';
}
artifactRouteId(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-');
}

View File

@@ -0,0 +1,71 @@
/**
* Unit tests for ReleasesListPageComponent -- DigestChipComponent adoption.
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { By } from '@angular/platform-browser';
import { ReleasesListPageComponent } from './releases-list-page.component';
import { DigestChipComponent } from '../../shared/domain/digest-chip/digest-chip.component';
describe('ReleasesListPageComponent - DigestChip adoption', () => {
let component: ReleasesListPageComponent;
let fixture: ComponentFixture<ReleasesListPageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReleasesListPageComponent],
providers: [provideRouter([])]
}).compileComponents();
fixture = TestBed.createComponent(ReleasesListPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should render DigestChipComponent for each release row', () => {
const digestChips = fixture.debugElement.queryAll(
By.directive(DigestChipComponent)
);
// The component uses fixture data; verify at least one chip renders
expect(digestChips.length).toBeGreaterThan(0);
});
it('should pass bundleDigest to each digest chip', () => {
const digestChips = fixture.debugElement.queryAll(
By.directive(DigestChipComponent)
);
for (const chip of digestChips) {
const chipInstance = chip.componentInstance as DigestChipComponent;
expect(chipInstance.digest).toBeTruthy();
// Digests should contain sha256: prefix
expect(chipInstance.digest).toContain('sha256:');
}
});
it('should use bundle 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('bundle');
}
});
it('should not have hand-rolled shortDigest method', () => {
// Verify the old method was removed
expect((component as any).shortDigest).toBeUndefined();
});
it('should not have hand-rolled copyDigest method', () => {
// Verify the old method was removed
expect((component as any).copyDigest).toBeUndefined();
});
});

View File

@@ -9,6 +9,7 @@ import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/c
import { RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { DigestChipComponent } from '../../shared/domain/digest-chip/digest-chip.component';
interface Release {
id: string;
@@ -24,7 +25,7 @@ interface Release {
@Component({
selector: 'app-releases-list-page',
standalone: true,
imports: [RouterLink, FormsModule],
imports: [RouterLink, FormsModule, DigestChipComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="releases-page">
@@ -95,15 +96,10 @@ interface Release {
</a>
</td>
<td>
<code class="digest" [title]="release.bundleDigest">
{{ shortDigest(release.bundleDigest) }}
</code>
<button
type="button"
class="copy-btn"
(click)="copyDigest(release.bundleDigest)"
title="Copy digest"
><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
<app-digest-chip
[digest]="release.bundleDigest"
variant="bundle"
></app-digest-chip>
</td>
<td>{{ release.components }}</td>
<td>{{ release.environment }}</td>
@@ -315,14 +311,6 @@ export class ReleasesListPageComponent {
});
});
shortDigest(digest: string): string {
const parts = digest.split(':');
if (parts.length === 2 && parts[1].length > 10) {
return `${parts[0]}:${parts[1].substring(0, 4)}...${parts[1].substring(parts[1].length - 3)}`;
}
return digest;
}
onSearch(query: string): void {
this.searchQuery.set(query);
}
@@ -363,14 +351,6 @@ export class ReleasesListPageComponent {
this.gateFilter.set(select.value);
}
async copyDigest(digest: string): Promise<void> {
try {
await navigator.clipboard.writeText(digest);
} catch (err) {
console.error('Failed to copy digest:', err);
}
}
openCreateRelease(): void {
console.log('Open create release modal');
}