Data Contract Governance
blockdevopsPayPal and OpenDataContract style Data Contracts specification editor, breaking change validator, and producer/consumer SLA agreements dashboard with YAML inspector, schema field explorer, live breaking change linter, and registered consumer status.
Installation
$pnpm dlx uipkge-ng@latest add data-contract-governance$npx uipkge-ng@latest add data-contract-governance$yarn dlx uipkge-ng@latest add data-contract-governance$bunx uipkge-ng@latest add data-contract-governanceManifest:
https://uipkge.dev/r/angular/data-contract-governance.jsonInstalls to:src/app/components/blocks/data-contract-governance/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SchemaFieldinterface SchemaField {
name: string
dataType: string
format?: string
nullable: boolean
isPrimaryKey?: boolean
isForeignKey?: boolean
foreignKeyRef?: string
isPii?: boolean
maskingStrategy?: string
constraints?: string
description: string
}LintTestRuleinterface LintTestRule {
id: string
name: string
category: 'breaking' | 'compatibility' | 'compliance'
categoryLabel: string
status: 'pass' | 'warn' | 'fail'
ruleDefinition: string
assertionPath: string
executionDuration: string
evaluatedAt: string
}ConsumerServiceinterface ConsumerService {
id: string
name: string
team: string
versionSubscribed: string
versionStatus: 'current' | 'minor-lag' | 'major-lag'
status: 'compliant' | 'warning' | 'deprecated'
throughput: string
latencyP99: string
slaStatus: string
compatibility: string
lastEventReceived: string
contact: string
}YamlLineinterface YamlLine {
text: string
className: string
}Files installed (2)
src/app/components/blocks/data-contract-governance/data-contract-governance.component.ts55 kB
import { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core' import { Activity, AlertCircle, AlertTriangle, Check, CheckCircle2, Code2, Copy, Download, FileCode, FileCode2, FileJson, HardDrive, Key, Link2, Loader2, Lock, LucideAngularModule, Play, Radio, RefreshCw, Search, Server, ShieldCheck, Table2, Tag, Terminal, Users, WrapText, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' export interface SchemaField { name: string dataType: string format?: string nullable: boolean isPrimaryKey?: boolean isForeignKey?: boolean foreignKeyRef?: string isPii?: boolean maskingStrategy?: string constraints?: string description: string } export interface LintTestRule { id: string name: string category: 'breaking' | 'compatibility' | 'compliance' categoryLabel: string status: 'pass' | 'warn' | 'fail' ruleDefinition: string assertionPath: string executionDuration: string evaluatedAt: string } export interface ConsumerService { id: string name: string team: string versionSubscribed: string versionStatus: 'current' | 'minor-lag' | 'major-lag' status: 'compliant' | 'warning' | 'deprecated' throughput: string latencyP99: string slaStatus: string compatibility: string lastEventReceived: string contact: string } const rawYamlContract = `apiVersion: datacontract.com/v3.0.1 kind: DataContract id: urn:datacontract:checkout:orders_placed info: title: orders_placed version: 3.2.0 status: enforced owner: Checkout Core Engineering Squad contact: #checkout-eng-alerts description: Canonical production data contract for completed checkout orders across global storefronts. servers: production: type: kafka topic: events.orders.placed format: json_schema cluster: prd-us-east-kafka.internal:9092 lakehouse: type: iceberg location: s3://lakehouse-analytics/tables/checkout/orders_placed_v3 format: parquet servicelevels: availability: percentage: 99.99% description: 30-day rolling cluster uptime latency: threshold: 100ms percentile: p99 freshness: maxDelay: 5s source: cdc_debezium_postgres retention: period: 365d tier: hot_kafka_7d_warm_iceberg_365d schema: type: object required: - order_id - customer_id - order_timestamp - total_amount_cents - currency_code - payment_method - customer_email_encrypted - shipping_country - line_items_count properties: order_id: type: string format: uuid nullable: false description: Unique primary transaction order UUID. customer_id: type: string format: uuid nullable: false description: Master verified customer identity reference. order_timestamp: type: string format: date-time pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" nullable: false description: Event generation timestamp in UTC RFC-3339. total_amount_cents: type: integer minimum: 0 maximum: 5000000 nullable: false description: Gross order value denominated in lowest currency unit. currency_code: type: string enum: ["USD", "EUR", "GBP", "JPY", "CAD"] nullable: false description: ISO 4217 three-letter currency code. payment_method: type: string enum: ["credit_card", "apple_pay", "google_pay", "sepa_debit"] nullable: false description: Authorized payment gateway instrument type. customer_email_encrypted: type: string nullable: false pii: true masking: deterministic_sha256 classification: restricted description: AES-GCM-256 encrypted customer email for fraud detection. shipping_country: type: string pattern: "^[A-Z]{2}$" nullable: false description: ISO 3166-1 alpha-2 destination country code. line_items_count: type: integer minimum: 1 maximum: 100 nullable: false description: Number of line items purchased in order.` const schemaFields: SchemaField[] = [ { name: 'order_id', dataType: 'string', format: 'uuid', nullable: false, isPrimaryKey: true, constraints: 'format: uuid (RFC-4122)', description: 'Unique primary transaction order identifier.', }, { name: 'customer_id', dataType: 'string', format: 'uuid', nullable: false, isForeignKey: true, foreignKeyRef: 'customers.customer_id', constraints: 'format: uuid, not null', description: 'Master verified customer identity reference.', }, { name: 'order_timestamp', dataType: 'string', format: 'date-time', nullable: false, constraints: 'pattern: RFC-3339 UTC', description: 'Event generation timestamp in strict ISO-8601 / RFC-3339 UTC.', }, { name: 'total_amount_cents', dataType: 'integer', format: 'int64', nullable: false, constraints: '0 ≤ value ≤ 5,000,000', description: 'Gross order value denominated in lowest currency unit (cents).', }, { name: 'currency_code', dataType: 'string', nullable: false, constraints: 'enum: [USD, EUR, GBP, JPY, CAD]', description: 'ISO-4217 three-letter currency code in which the charge occurred.', }, { name: 'payment_method', dataType: 'string', nullable: false, constraints: 'enum: [credit_card, apple_pay, google_pay, sepa_debit]', description: 'Authorized payment gateway instrument method.', }, { name: 'customer_email_encrypted', dataType: 'string', nullable: false, isPii: true, maskingStrategy: 'SHA-256 / AES-256-GCM', constraints: 'pii: true, restricted', description: 'Cryptographically enveloped customer email address.', }, { name: 'shipping_country', dataType: 'string', nullable: false, constraints: 'pattern: ^[A-Z]{2}$ (ISO 3166-1)', description: 'Two-letter alpha-2 shipping destination country code.', }, { name: 'line_items_count', dataType: 'integer', format: 'int32', nullable: false, constraints: '1 ≤ value ≤ 100', description: 'Total count of individual catalog items inside the basket.', }, ] const lintRules: LintTestRule[] = [ { id: 'lint-1', name: "Field 'customer_id' remains required", category: 'breaking', categoryLabel: 'Requiredness Invariant', status: 'pass', ruleDefinition: 'Required string UUID field cannot be made optional or omitted without a major semantic version bump (v4.0.0).', assertionPath: 'schema.properties.customer_id.nullable == false', executionDuration: '14ms', evaluatedAt: 'Just now', }, { id: 'lint-2', name: "Field 'currency_code' enum compatibility", category: 'compatibility', categoryLabel: 'Enum Evolution', status: 'pass', ruleDefinition: 'Allowed currency enum set [USD, EUR, GBP, JPY, CAD] is a strict superset of v3.1; no historical enum values were pruned.', assertionPath: 'schema.properties.currency_code.enum (5 items)', executionDuration: '22ms', evaluatedAt: 'Just now', }, { id: 'lint-3', name: 'Timestamp ISO-8601 format compliance', category: 'compliance', categoryLabel: 'Format Strictness', status: 'pass', ruleDefinition: "Field 'order_timestamp' adheres to strict RFC 3339 / ISO-8601 UTC regex pattern with mandatory 'Z' zone identifier.", assertionPath: 'format: date-time (RFC-3339 UTC)', executionDuration: '18ms', evaluatedAt: 'Just now', }, { id: 'lint-4', name: 'Backward compatibility for existing consumer v3.1', category: 'compatibility', categoryLabel: 'Consumer Simulation', status: 'pass', ruleDefinition: 'Simulated payload deserialization passed across all 8 active consumer test vectors without JSON schema deserializer exceptions.', assertionPath: '8/8 consumer test vectors pass', executionDuration: '145ms', evaluatedAt: 'Just now', }, { id: 'lint-5', name: "Field 'customer_email_encrypted' PII Masking Rule", category: 'compliance', categoryLabel: 'Security & PII', status: 'pass', ruleDefinition: 'Customer email field enforces cryptographic ciphertext masking (AES-GCM-256 / SHA-256) prior to publishing onto Kafka broker.', assertionPath: 'pii: true · masking: deterministic_sha256', executionDuration: '31ms', evaluatedAt: 'Just now', }, { id: 'lint-6', name: 'Schema type narrowing safety', category: 'breaking', categoryLabel: 'Type Evolution', status: 'pass', ruleDefinition: 'No existing schema properties had primitive data types narrowed or widened in a breaking manner (e.g. integer to short).', assertionPath: '0 type narrowing violations detected', executionDuration: '19ms', evaluatedAt: 'Just now', }, ] const consumerServices: ConsumerService[] = [ { id: 'svc-1', name: 'Fraud Detection Service', team: 'Security & Risk Squad', versionSubscribed: 'v3.2.0', versionStatus: 'current', status: 'compliant', throughput: '4,200 msg/s', latencyP99: '38ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: '100% Compatible', lastEventReceived: '1.2s ago', contact: '#fraud-eng', }, { id: 'svc-2', name: 'Financial Ledger Mart', team: 'Finance Analytics Squad', versionSubscribed: 'v3.1.0', versionStatus: 'minor-lag', status: 'compliant', throughput: '1,850 msg/s', latencyP99: '52ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: 'Backward Compat Verified', lastEventReceived: '3.4s ago', contact: '#fin-ledger-team', }, { id: 'svc-3', name: 'Email Notification Dispatcher', team: 'Customer Comms Team', versionSubscribed: 'v3.2.0', versionStatus: 'current', status: 'compliant', throughput: '850 msg/s', latencyP99: '24ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: '100% Compatible', lastEventReceived: '0.8s ago', contact: '#comms-alerts', }, { id: 'svc-4', name: 'Inventory Fulfillment Pipeline', team: 'Supply Chain Engineering', versionSubscribed: 'v3.2.0', versionStatus: 'current', status: 'compliant', throughput: '2,100 msg/s', latencyP99: '34ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: '100% Compatible', lastEventReceived: '1.5s ago', contact: '#supply-chain-dev', }, { id: 'svc-5', name: 'Customer Data Platform (CDP)', team: 'Growth & Marketing Tech', versionSubscribed: 'v3.0.4', versionStatus: 'minor-lag', status: 'compliant', throughput: '3,400 msg/s', latencyP99: '48ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: 'v3.0 Compat Mode', lastEventReceived: '2.1s ago', contact: '#growth-infra', }, { id: 'svc-6', name: 'Realtime Order Tracking Gateway', team: 'Mobile & Web Edge Platform', versionSubscribed: 'v3.2.0', versionStatus: 'current', status: 'compliant', throughput: '5,600 msg/s', latencyP99: '19ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: '100% Compatible', lastEventReceived: '0.4s ago', contact: '#edge-team', }, { id: 'svc-7', name: 'Audit Log & Compliance Vault', team: 'Infosec & Legal Systems', versionSubscribed: 'v3.2.0', versionStatus: 'current', status: 'compliant', throughput: '1,200 msg/s', latencyP99: '28ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: '100% Compatible', lastEventReceived: '4.2s ago', contact: '#infosec-ops', }, { id: 'svc-8', name: 'Search & Recommendation Indexer', team: 'Discovery & ML Platform', versionSubscribed: 'v3.1.2', versionStatus: 'minor-lag', status: 'compliant', throughput: '980 msg/s', latencyP99: '31ms', slaStatus: 'SLA Compliant (p99 < 100ms)', compatibility: 'Backward Compat Verified', lastEventReceived: '1.9s ago', contact: '#discovery-ml', }, ] export type DataContractInspectorTab = 'yaml' | 'schema' | 'sla' export type DataContractLinterFilter = 'all' | 'breaking' | 'compatibility' | 'compliance' export interface YamlLine { text: string className: string } /** Colour one YAML line the way the React viewer does (keywords, sections, PII, comments). */ export function yamlLineClass(line: string): string { const isKeyword = line.startsWith('apiVersion:') || line.startsWith('kind:') || line.startsWith('id:') const isSection = line.startsWith('info:') || line.startsWith('servers:') || line.startsWith('servicelevels:') || line.startsWith('schema:') const isPii = line.includes('pii: true') || line.includes('masking:') const isComment = line.trim().startsWith('#') if (isKeyword) return 'text-info font-semibold' if (isSection) return 'text-chart-1 font-semibold' if (isPii) return 'text-success font-semibold' if (isComment) return 'text-muted-foreground/80 italic' return 'text-foreground/90' } const yamlLines: YamlLine[] = rawYamlContract.split('\n').map((text) => ({ text, className: yamlLineClass(text) })) const filterButtonBase = 'h-7 px-2.5 text-xs font-medium' const filterButtonActive = 'bg-background text-foreground shadow-xs' const filterButtonIdle = 'text-muted-foreground hover:text-foreground' /** * DataContractGovernance -- the React block, 1:1: an OpenDataContract style contract page with * a header (title, semver + enforced badges, ownership strip, Export JSON Schema and Validate * actions with a live feedback strip), four governance KPI cards, a left inspector with YAML / * schema fields / SLA tabs, and a right column with the breaking change linter (filterable, * re-runnable) and the searchable registered consumers table. Edit the data consts above. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-data-contract-governance, [ui-data-contract-governance]', standalone: true, // React renders the root <div> itself: keep this wrapper out of the layout. host: { class: 'contents' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiInputComponent, UiSeparatorComponent, UiTableComponent, UiTableHeaderComponent, UiTableBodyComponent, UiTableRowComponent, UiTableHeadComponent, UiTableCellComponent, UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent, UiTabsContentComponent, ], template: ` <div data-slot="data-contract-governance" [class]="rootClass"> <!-- TOP HEADER --> <header class="border-border/80 bg-muted/20 border-b p-4 sm:p-6"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <!-- Left: Contract Title, Version, Status Badges --> <div class="space-y-2"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-primary/10 text-primary border-primary/20 flex size-8 items-center justify-center rounded-lg border" > <lucide-icon [img]="FileCode2" class="size-4" /> </div> <div> <div class="flex flex-wrap items-center gap-2"> <h2 class="font-mono text-base font-bold break-all sm:text-lg">{{ contractTitle }}</h2> <span ui-badge variant="secondary" class="gap-1 font-mono text-xs font-medium"> <lucide-icon [img]="Tag" class="text-info size-3" /> {{ version }} · Semantic Versioned </span> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5 font-mono text-xs font-semibold" > <span class="bg-success size-1.5 animate-pulse rounded-full"></span> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" /> Contract Enforced & Passing </span> </div> </div> </div> <!-- Metadata Badges & Ownership Strip --> <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1.5 font-mono text-xs"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Users" class="text-info size-3" /> <span>Owner:</span> <span class="text-foreground font-medium">{{ ownerTeam }}</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Server" class="text-chart-1 size-3" /> <span>Producer:</span> <span class="text-foreground font-medium">{{ producerService }}</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Radio" class="text-warning size-3" /> <span>Topic:</span> <span class="text-foreground font-medium">{{ destinationTopic }}</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Lock" class="text-success size-3" /> <span>Classification:</span> <span class="text-foreground font-medium">PII Sensitive · Encrypted</span> </div> </div> </div> <!-- Right: Primary & Secondary Action Buttons --> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs font-medium" (click)="handleExportSchema()" > <lucide-icon [img]="Download" class="text-info size-3.5" /> <span>Export Schema (JSON Schema)</span> </button> <button ui-button variant="default" size="sm" class="bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 text-xs font-semibold shadow-xs" [disabled]="isValidatingYaml()" (click)="handleValidateContract()" > @if (isValidatingYaml()) { <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" /> } @else { <lucide-icon [img]="Play" class="size-3.5 fill-current" /> } <span>{{ isValidatingYaml() ? 'Validating Contract...' : 'Validate Contract YAML' }}</span> </button> </div> </div> <!-- Live Feedback Notification --> @if (validationToast()) { <div class="border-border bg-background mt-3 flex items-center justify-between rounded-lg border px-3 py-2 text-xs" > <div class="flex items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="text-success size-4" /> <span class="text-foreground font-mono font-medium">{{ validationToast() }}</span> </div> <span class="text-muted-foreground font-mono text-xs">{{ lastValidatedTimestamp() }}</span> </div> } </header> <!-- 4 DATA GOVERNANCE KPI CARDS --> <div class="border-border/70 bg-border grid grid-cols-1 gap-px border-b sm:grid-cols-2 lg:grid-cols-4"> <!-- Card 1: Registered Consumers --> <div class="bg-card p-4 sm:p-5"> <div class="flex items-center justify-between pb-2"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Registered Consumers </span> <div class="border-info/20 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border"> <lucide-icon [img]="Users" class="size-3.5" /> </div> </div> <div class="text-2xl font-bold tracking-tight">8 Services</div> <div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-xs"> <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info text-xs font-normal"> 8 Downstream Consumer Services </span> <span class="text-muted-foreground">across 3 domains</span> </div> </div> <!-- Card 2: Breaking Changes Detected --> <div class="bg-card p-4 sm:p-5"> <div class="flex items-center justify-between pb-2"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Breaking Changes</span> <div class="border-success/20 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="ShieldCheck" class="size-3.5" /> </div> </div> <div class="text-success text-2xl font-bold tracking-tight">0 Breaking</div> <div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-xs"> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium"> 0 Breaking Changes </span> <span class="text-muted-foreground">100% backward compat</span> </div> </div> <!-- Card 3: Schema Freshness SLA --> <div class="bg-card p-4 sm:p-5"> <div class="flex items-center justify-between pb-2"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Freshness SLA</span> <div class="border-warning/20 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="Activity" class="size-3.5" /> </div> </div> <div class="text-2xl font-bold tracking-tight">99.9% CDC</div> <div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-xs"> <span ui-badge variant="outline" class="border-warning/30 bg-warning/10 text-warning text-xs font-normal"> SLA: 99.9% · Real-time CDC </span> <span class="text-muted-foreground">p99 < 85ms</span> </div> </div> <!-- Card 4: Quality Assertions Bound --> <div class="bg-card p-4 sm:p-5"> <div class="flex items-center justify-between pb-2"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Quality Assertions </span> <div class="border-chart-2/20 bg-chart-2/10 text-chart-2 flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="CheckCircle2" class="size-3.5" /> </div> </div> <div class="text-2xl font-bold tracking-tight">14 Active</div> <div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-xs"> <span ui-badge variant="outline" class="border-chart-2/30 bg-chart-2/10 text-chart-2 text-xs font-normal"> 14 Schema Constraints Active </span> <span class="text-muted-foreground">14/14 passing</span> </div> </div> </div> <!-- 2-COLUMN DATA CONTRACT STUDIO --> <div class="divide-border grid flex-1 grid-cols-1 divide-y lg:grid-cols-12 lg:divide-x lg:divide-y-0"> <!-- LEFT COLUMN: CONTRACT SCHEMA & SLA INSPECTOR --> <section class="bg-muted/10 flex flex-col space-y-4 p-4 sm:p-5 lg:col-span-5"> <!-- Section Header --> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <lucide-icon [img]="Code2" class="text-primary size-4" /> <h3 class="text-sm font-semibold">Contract Specification & SLA</h3> </div> <p class="text-muted-foreground text-xs"> OpenDataContract v3.0 schema definitions and producer guarantees </p> </div> </div> <!-- Tabs Container --> <ui-tabs [value]="activeInspectorTab()" (valueChange)="setInspectorTab($event)" class="w-full"> <div class="flex items-center justify-between gap-2 pb-1"> <div ui-tabs-list variant="segmented" class="h-8"> <button ui-tabs-trigger value="yaml" class="h-7 gap-1 px-2.5 text-xs"> <lucide-icon [img]="FileCode" class="size-3.5" /> <span>YAML Spec</span> </button> <button ui-tabs-trigger value="schema" class="h-7 gap-1 px-2.5 text-xs"> <lucide-icon [img]="Table2" class="size-3.5" /> <span>Fields ({{ schemaFields.length }})</span> </button> <button ui-tabs-trigger value="sla" class="h-7 gap-1 px-2.5 text-xs"> <lucide-icon [img]="Activity" class="size-3.5" /> <span>SLA Agreements</span> </button> </div> <!-- Actions per tab --> <div class="flex items-center gap-1"> @if (activeInspectorTab() === 'yaml') { <button ui-button variant="ghost" size="sm" class="h-7 px-2 text-xs" [attr.title]="wrapYamlLines() ? 'Disable line wrap' : 'Enable line wrap'" (click)="wrapYamlLines.set(!wrapYamlLines())" > <lucide-icon [img]="WrapText" class="size-3.5" /> </button> <button ui-button variant="ghost" size="sm" class="h-7 gap-1 px-2 text-xs" (click)="handleCopyYaml()"> @if (copiedYaml()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ copiedYaml() ? 'Copied' : 'Copy' }}</span> </button> } @if (activeInspectorTab() === 'schema') { <button ui-button variant="ghost" size="sm" class="h-7 gap-1 px-2 text-xs" (click)="handleCopyJsonSchema()" > @if (copiedSchema()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="FileJson" class="size-3.5" /> } <span>{{ copiedSchema() ? 'Copied JSON' : 'Copy JSON' }}</span> </button> } </div> </div> <!-- TAB 1: YAML SPECIFICATION --> <ui-tabs-content value="yaml" class="mt-2 space-y-3 focus-visible:outline-hidden"> <div class="border-border bg-muted/40 relative overflow-hidden rounded-lg border font-mono text-xs shadow-xs" > <!-- Code Header Bar --> <div class="border-border/80 bg-muted/70 flex items-center justify-between border-b px-3 py-1.5 text-xs" > <div class="flex items-center gap-2"> <span class="bg-success size-2 rounded-full"></span> <span class="text-foreground font-semibold">orders_placed_v3.contract.yaml</span> </div> <div class="text-muted-foreground flex items-center gap-2"> <span>UTF-8</span> <span>YAML 1.2</span> </div> </div> <!-- Monospace YAML Viewer (one line per row: no stray whitespace under whitespace-pre) --> <div [class]="yamlViewerClass()"> @for (line of yamlLines; track $index) { <div class="hover:bg-muted/50 flex leading-5"> <span class="text-muted-foreground/60 w-7 shrink-0 pr-3 text-right font-mono text-xs select-none" >{{ $index + 1 }}</span ><span [class]="line.className">{{ line.text }}</span> </div> } </div> </div> <!-- Routing Details Strip --> <ui-card class="border-border bg-card p-3 shadow-none"> <div class="grid grid-cols-2 gap-2 font-mono text-xs"> <div class="space-y-0.5"> <span class="text-muted-foreground">Kafka Cluster</span> <div class="text-foreground truncate font-medium">prd-us-east-kafka.internal</div> </div> <div class="space-y-0.5"> <span class="text-muted-foreground">Lakehouse Sync</span> <div class="text-foreground truncate font-medium">Apache Iceberg (Parquet)</div> </div> </div> </ui-card> </ui-tabs-content> <!-- TAB 2: SCHEMA FIELDS EXPLORER --> <ui-tabs-content value="schema" class="mt-2 space-y-3 focus-visible:outline-hidden"> <div class="border-border bg-card overflow-hidden rounded-lg border"> <div ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Field Name </th> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold">Type</th> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Nullability </th> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Tags / Constraints </th> </tr> </thead> <tbody ui-table-body class="font-mono text-xs"> @for (field of schemaFields; track field.name) { <tr ui-table-row class="hover:bg-muted/30"> <td ui-table-cell class="py-2.5 font-medium"> <div class="flex items-center gap-1.5"> @if (field.isPrimaryKey) { <lucide-icon [img]="Key" class="text-warning size-3" /> } @else if (field.isForeignKey) { <lucide-icon [img]="Link2" class="text-info size-3" /> } @else if (field.isPii) { <lucide-icon [img]="Lock" class="text-success size-3" /> } <span class="text-foreground">{{ field.name }}</span> </div> </td> <td ui-table-cell class="py-2.5"> <span ui-badge variant="secondary" class="font-mono text-xs" >{{ field.dataType }}{{ field.format ? ' (' + field.format + ')' : '' }}</span > </td> <td ui-table-cell class="py-2.5"> <span ui-badge [variant]="field.nullable ? 'outline' : 'default'" class="text-xs">{{ field.nullable ? 'nullable' : 'required' }}</span> </td> <td ui-table-cell class="py-2.5"> <div class="flex flex-col gap-0.5"> @if (field.isPii) { <span class="text-success font-semibold">PII: {{ field.maskingStrategy }}</span> } <span class="text-muted-foreground text-xs">{{ field.constraints }}</span> </div> </td> </tr> } </tbody> </div> </div> </ui-tabs-content> <!-- TAB 3: SLA SERVICE LEVEL AGREEMENTS --> <ui-tabs-content value="sla" class="mt-2 space-y-3 focus-visible:outline-hidden"> <div class="grid grid-cols-1 gap-3"> <!-- SLA Item 1: Latency --> <ui-card class="border-border bg-card p-3.5 shadow-none"> <div class="flex items-start justify-between"> <div class="space-y-1"> <div class="flex items-center gap-1.5 text-xs font-semibold"> <lucide-icon [img]="Zap" class="text-warning size-3.5" /> <span>Event Latency (p99)</span> </div> <p class="text-muted-foreground text-xs"> End-to-end event publication latency from checkout commit to Kafka broker. </p> </div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-mono text-xs" > Target < 100ms </span> </div> <div class="border-border/60 bg-muted/30 mt-3 flex items-center justify-between rounded-md border p-2 font-mono text-xs" > <span class="text-muted-foreground">Observed 30-Day p99:</span> <span class="text-success font-bold">42ms (Passing)</span> </div> </ui-card> <!-- SLA Item 2: Availability --> <ui-card class="border-border bg-card p-3.5 shadow-none"> <div class="flex items-start justify-between"> <div class="space-y-1"> <div class="flex items-center gap-1.5 text-xs font-semibold"> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" /> <span>Producer Availability</span> </div> <p class="text-muted-foreground text-xs"> Uptime commitment for producer event emission without message drops. </p> </div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-mono text-xs" > 99.99% Guaranteed </span> </div> <div class="border-border/60 bg-muted/30 mt-3 flex items-center justify-between rounded-md border p-2 font-mono text-xs" > <span class="text-muted-foreground">Actual Rolling Uptime:</span> <span class="text-success font-bold">99.995%</span> </div> </ui-card> <!-- SLA Item 3: Retention & Archival --> <ui-card class="border-border bg-card p-3.5 shadow-none"> <div class="flex items-start justify-between"> <div class="space-y-1"> <div class="flex items-center gap-1.5 text-xs font-semibold"> <lucide-icon [img]="HardDrive" class="text-info size-3.5" /> <span>Data Retention & Tiering</span> </div> <p class="text-muted-foreground text-xs"> Hot retention on Apache Kafka broker topic with cold Iceberg archival. </p> </div> <span ui-badge variant="outline" class="font-mono text-xs">365 Days</span> </div> <div class="border-border/60 bg-muted/30 mt-3 flex items-center justify-between rounded-md border p-2 font-mono text-xs" > <span class="text-muted-foreground">Storage Tiers:</span> <span class="text-foreground font-medium">Kafka 7d · Iceberg 365d</span> </div> </ui-card> <!-- SLA Item 4: Escalation Contacts --> <ui-card class="border-border bg-card p-3.5 shadow-none"> <div class="space-y-1"> <div class="flex items-center gap-1.5 text-xs font-semibold"> <lucide-icon [img]="Users" class="text-chart-2 size-3.5" /> <span>On-Call SLA Escalation</span> </div> <p class="text-muted-foreground text-xs"> Tier 1 Mission Critical Producer Squad: MTTA < 5 mins, MTTR < 15 mins. </p> </div> <div class="border-border/60 bg-muted/30 mt-2.5 flex items-center justify-between rounded-md border p-2 font-mono text-xs" > <span class="text-muted-foreground">Slack & PagerDuty:</span> <span class="text-primary font-medium">#checkout-eng-alerts</span> </div> </ui-card> </div> </ui-tabs-content> </ui-tabs> </section> <!-- RIGHT COLUMN: BREAKING CHANGE LINTER & VALIDATION ENGINE --> <section class="bg-card flex flex-col space-y-6 p-4 sm:p-5 lg:col-span-7"> <!-- Linter Section Header --> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="text-primary size-4" /> <h3 class="text-sm font-semibold">Breaking Change Linter & Validation Engine</h3> </div> <p class="text-muted-foreground text-xs"> Automated schema drift detection and backward compatibility verification </p> </div> <!-- Linter Action Controls --> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="h-7 gap-1.5 text-xs font-medium" [disabled]="isRunningLinter()" (click)="handleRunLinter()" > <lucide-icon [img]="RefreshCw" [class]="linterIconClass()" /> <span>{{ isRunningLinter() ? 'Linting...' : 'Re-run Linter' }}</span> </button> </div> </div> <!-- Linter Filters & Summary Bar --> <div class="border-border/70 bg-muted/20 flex flex-col gap-2 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between" > <div class="flex flex-wrap items-center gap-1.5"> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('all')" (click)="selectedLinterFilter.set('all')" > All Rules ({{ lintRules.length }}) </button> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('breaking')" (click)="selectedLinterFilter.set('breaking')" > Breaking Invariants (2) </button> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('compatibility')" (click)="selectedLinterFilter.set('compatibility')" > Compatibility (2) </button> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('compliance')" (click)="selectedLinterFilter.set('compliance')" > Compliance & PII (2) </button> </div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-mono text-xs font-semibold" > {{ passingRulesCount }}/{{ lintRules.length }} Passing (100%) </span> </div> <!-- LIVE BREAKING CHANGE TEST RUNNER CARDS --> <div class="space-y-2.5"> @for (rule of filteredLintRules(); track rule.id) { <div class="border-border bg-card/60 hover:bg-muted/20 rounded-lg border p-3 transition-colors"> <div class="flex items-start justify-between gap-3"> <div class="flex items-start gap-2.5"> <div class="mt-0.5"> @if (rule.status === 'pass') { <lucide-icon [img]="CheckCircle2" class="text-success size-4" /> } @else if (rule.status === 'warn') { <lucide-icon [img]="AlertTriangle" class="text-warning size-4" /> } @else { <lucide-icon [img]="AlertCircle" class="text-destructive size-4" /> } </div> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <span class="text-foreground font-mono text-xs font-bold">{{ rule.name }}</span> <span ui-badge variant="secondary" class="font-mono text-xs font-normal"> {{ rule.categoryLabel }} </span> </div> <p class="text-muted-foreground text-xs leading-relaxed">{{ rule.ruleDefinition }}</p> <div class="border-border/60 bg-muted/40 text-muted-foreground inline-flex items-center gap-1.5 rounded px-2 py-0.5 font-mono text-xs" > <lucide-icon [img]="Terminal" class="text-info size-3" /> <span>{{ rule.assertionPath }}</span> </div> </div> </div> <div class="flex shrink-0 flex-col items-end gap-1"> <span ui-badge [variant]="rule.status === 'pass' ? 'outline' : 'destructive'" class="border-success/30 bg-success/10 text-success font-mono text-xs font-bold" > PASS </span> <span class="text-muted-foreground font-mono text-xs">{{ rule.executionDuration }}</span> </div> </div> </div> } </div> <ui-separator class="my-2" /> <!-- ACTIVE CONSUMERS LIST --> <div class="space-y-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Registered Downstream Consumers ({{ consumerServices.length }}) </h4> <p class="text-muted-foreground text-xs"> Active consumer services subscribing to topic <code class="text-foreground font-mono">{{ destinationTopic }}</code> </p> </div> <!-- Consumer Search Input --> <div class="relative w-full sm:w-56"> <!-- [class] (not class=) styles only the svg: the absolute offsets must not apply twice. --> <lucide-icon [img]="Search" [class]="'text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2'" /> <ui-input [value]="consumerSearch()" (valueChange)="consumerSearch.set($event)" placeholder="Filter consumers..." class="h-7 pl-8 font-mono text-xs" /> </div> </div> <!-- Consumers Table --> <div class="border-border bg-card overflow-hidden rounded-lg border"> <div ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Consumer Service </th> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Subscribed Ver </th> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Throughput & SLA </th> <th ui-table-head class="text-muted-foreground h-8 font-mono text-xs font-semibold"> Compatibility </th> </tr> </thead> <tbody ui-table-body class="font-mono text-xs"> @for (consumer of filteredConsumers(); track consumer.id) { <tr ui-table-row class="hover:bg-muted/30"> <td ui-table-cell class="py-2.5"> <div class="space-y-0.5"> <div class="text-foreground font-bold">{{ consumer.name }}</div> <div class="text-muted-foreground text-xs">{{ consumer.team }} · {{ consumer.contact }}</div> </div> </td> <td ui-table-cell class="py-2.5"> <span ui-badge [variant]="consumer.versionStatus === 'current' ? 'secondary' : 'outline'" class="font-mono text-xs" > {{ consumer.versionSubscribed }} </span> </td> <td ui-table-cell class="py-2.5"> <div class="space-y-0.5"> <div class="text-foreground font-medium">{{ consumer.throughput }}</div> <div class="text-success text-xs">p99: {{ consumer.latencyP99 }} (OK)</div> </div> </td> <td ui-table-cell class="py-2.5"> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-mono text-xs font-medium" > <lucide-icon [img]="Check" class="text-success size-3" /> {{ consumer.compatibility }} </span> </td> </tr> } </tbody> </div> </div> </div> </section> </div> </div> `, }) export class UiDataContractGovernanceComponent { @Input() contractTitle = 'orders_placed_v3.contract.yaml' @Input() version = 'v3.2.0' @Input() ownerTeam = 'Checkout Core Engineering Squad' @Input() producerService = 'checkout-service-prod' @Input() destinationTopic = 'events.orders.placed' @Input() lakehouseTarget = 's3://lakehouse-analytics/tables/checkout/orders_placed_v3' @Input('class') className?: string protected readonly FileCode2 = FileCode2 protected readonly Tag = Tag protected readonly ShieldCheck = ShieldCheck protected readonly Users = Users protected readonly Server = Server protected readonly Radio = Radio protected readonly Lock = Lock protected readonly Download = Download protected readonly Loader2 = Loader2 protected readonly Play = Play protected readonly Activity = Activity protected readonly CheckCircle2 = CheckCircle2 protected readonly Code2 = Code2 protected readonly FileCode = FileCode protected readonly Table2 = Table2 protected readonly WrapText = WrapText protected readonly Check = Check protected readonly Copy = Copy protected readonly FileJson = FileJson protected readonly Key = Key protected readonly Link2 = Link2 protected readonly Zap = Zap protected readonly HardDrive = HardDrive protected readonly RefreshCw = RefreshCw protected readonly AlertTriangle = AlertTriangle protected readonly AlertCircle = AlertCircle protected readonly Terminal = Terminal protected readonly Search = Search protected readonly schemaFields = schemaFields protected readonly lintRules = lintRules protected readonly consumerServices = consumerServices protected readonly yamlLines = yamlLines protected readonly passingRulesCount = lintRules.filter((r) => r.status === 'pass').length readonly activeInspectorTab = signal<DataContractInspectorTab>('yaml') readonly selectedLinterFilter = signal<DataContractLinterFilter>('all') readonly isValidatingYaml = signal(false) readonly validationToast = signal<string | null>(null) readonly isRunningLinter = signal(false) readonly copiedYaml = signal(false) readonly copiedSchema = signal(false) readonly wrapYamlLines = signal(false) readonly consumerSearch = signal('') readonly lastValidatedTimestamp = signal('Just now') readonly filteredLintRules = computed(() => { const f = this.selectedLinterFilter() if (f === 'all') return lintRules return lintRules.filter((r) => r.category === f) }) readonly filteredConsumers = computed(() => { const q = this.consumerSearch().trim().toLowerCase() if (!q) return consumerServices return consumerServices.filter( (c) => c.name.toLowerCase().includes(q) || c.team.toLowerCase().includes(q) || c.versionSubscribed.toLowerCase().includes(q) || c.compatibility.toLowerCase().includes(q), ) }) private readonly timers = new Set<ReturnType<typeof setTimeout>>() constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } protected get rootClass(): string { return cn( 'border-border bg-card text-foreground flex w-full flex-col overflow-hidden rounded-xl border shadow-xs', this.className, ) } protected yamlViewerClass(): string { return cn('max-h-[540px] overflow-auto p-3', this.wrapYamlLines() ? 'whitespace-pre-wrap' : 'whitespace-pre') } protected linterIconClass(): string { return cn('size-3', this.isRunningLinter() && 'animate-spin') } protected filterButtonClass(filter: DataContractLinterFilter): string { return cn(filterButtonBase, this.selectedLinterFilter() === filter ? filterButtonActive : filterButtonIdle) } setInspectorTab(value: string): void { this.activeInspectorTab.set(value as DataContractInspectorTab) } private later(fn: () => void, ms: number): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } handleValidateContract(): void { if (this.isValidatingYaml()) return this.isValidatingYaml.set(true) this.validationToast.set('Validating 6 linter invariants & 8 consumer vectors...') this.later(() => { this.isValidatingYaml.set(false) this.lastValidatedTimestamp.set('Just now') this.validationToast.set('Contract Validated · 0 Breaking Changes · 100% Compatible') this.later(() => this.validationToast.set(null), 3500) }, 750) } handleRunLinter(): void { if (this.isRunningLinter()) return this.isRunningLinter.set(true) this.later(() => this.isRunningLinter.set(false), 600) } handleCopyYaml(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(rawYamlContract) this.copiedYaml.set(true) this.later(() => this.copiedYaml.set(false), 2000) } } handleCopyJsonSchema(): void { const jsonSchemaObj = { $schema: 'http://json-schema.org/draft-07/schema#', title: 'orders_placed', description: 'Canonical production data contract for completed checkout orders across global storefronts.', type: 'object', required: schemaFields.filter((f) => !f.nullable).map((f) => f.name), properties: Object.fromEntries( schemaFields.map((f) => [ f.name, { type: f.dataType, ...(f.format ? { format: f.format } : {}), description: f.description, ...(f.isPii ? { 'x-pii': true, 'x-masking': f.maskingStrategy } : {}), }, ]), ), } const jsonStr = JSON.stringify(jsonSchemaObj, null, 2) if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(jsonStr) this.copiedSchema.set(true) this.later(() => this.copiedSchema.set(false), 2000) } } handleExportSchema(): void { const jsonSchemaObj = { $schema: 'http://json-schema.org/draft-07/schema#', id: 'urn:datacontract:checkout:orders_placed', version: this.version, title: 'orders_placed', owner: this.ownerTeam, producer: this.producerService, topic: this.destinationTopic, exportedAt: new Date().toISOString(), servicelevels: { availability: '99.99%', latencyP99: '< 100ms', freshness: '< 5s CDC', retention: '365d', }, schema: { type: 'object', required: schemaFields.filter((f) => !f.nullable).map((f) => f.name), properties: Object.fromEntries( schemaFields.map((f) => [ f.name, { type: f.dataType, ...(f.format ? { format: f.format } : {}), description: f.description, constraints: f.constraints, ...(f.isPii ? { 'x-pii': true, 'x-masking': f.maskingStrategy } : {}), }, ]), ), }, } const blobData = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(jsonSchemaObj, null, 2)) if (typeof document !== 'undefined') { const link = document.createElement('a') link.setAttribute('href', blobData) link.setAttribute('download', `${this.contractTitle.replace('.yaml', '')}.json`) document.body.appendChild(link) link.click() link.remove() } } }src/app/components/blocks/data-contract-governance/index.ts0.3 kB
export { UiDataContractGovernanceComponent, yamlLineClass, type ConsumerService, type DataContractInspectorTab, type DataContractLinterFilter, type LintTestRule, type SchemaField, type YamlLine, } from './data-contract-governance.component'
Raw manifest:https://uipkge.dev/r/angular/data-contract-governance.json