Data Product Card
blockdevopsData Mesh architectural Data Product Card (DPC) featuring SLA compliance health, multi-modal output ports (Snowflake, Kafka, GraphQL, Parquet S3), domain ownership, consumer counts, and access request triggers.
Installation
$pnpm dlx uipkge-ng@latest add data-product-card$npx uipkge-ng@latest add data-product-card$yarn dlx uipkge-ng@latest add data-product-card$bunx uipkge-ng@latest add data-product-cardManifest:
https://uipkge.dev/r/angular/data-product-card.jsonInstalls to:src/app/components/blocks/data-product-card/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.
OutputPortinterface OutputPort {
id: string
name: string
type: 'snowflake' | 'kafka' | 'graphql' | 's3' | 'postgres'
typeLabel: string
uri: string
latency: string
status: 'online' | 'degraded' | 'syncing'
}DataProductDatainterface DataProductData {
id: string
name: string
version: string
domain: string
tier: 'Gold Tier 1' | 'Silver Tier 2' | 'Bronze Tier 3'
description: string
slaCompliance: number
freshness: string
completenessPct: number
activeConsumersCount: number
owner: {
name: string
role: string
team: string
avatar?: string
fallback: string
}
outputPorts: OutputPort[]
tags: string[]
certifiedCompliance: string
lastUpdated: string
}Files installed (2)
src/app/components/blocks/data-product-card/data-product-card.component.ts18.9 kB
import { ChangeDetectionStrategy, Component, Input, OnDestroy, signal } from '@angular/core' import { Activity, Award, Check, ChevronRight, Copy, Database, HardDrive, KeyRound, Layers, LucideAngularModule, Radio, Server, ShieldCheck, Users, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export interface OutputPort { id: string name: string type: 'snowflake' | 'kafka' | 'graphql' | 's3' | 'postgres' typeLabel: string uri: string latency: string status: 'online' | 'degraded' | 'syncing' } export interface DataProductData { id: string name: string version: string domain: string tier: 'Gold Tier 1' | 'Silver Tier 2' | 'Bronze Tier 3' description: string slaCompliance: number freshness: string completenessPct: number activeConsumersCount: number owner: { name: string role: string team: string avatar?: string fallback: string } outputPorts: OutputPort[] tags: string[] certifiedCompliance: string lastUpdated: string } export const DEFAULT_DATA_PRODUCT: DataProductData = { id: 'dp-cust-360', name: 'dp_customer_360_profile', version: 'v3.2.0', domain: 'Customer Intelligence & Growth', tier: 'Gold Tier 1', description: 'Unified customer identity graph consolidating CRM accounts, Stripe transaction volumes, and behavioral product telemetry into clean semantic marts.', slaCompliance: 99.8, freshness: 'Updated 8m ago · Hourly sync', completenessPct: 99.4, activeConsumersCount: 14, owner: { name: 'Marcus Vance', role: 'Staff Data Architect', team: 'Data Platform Squad', fallback: 'MV', }, outputPorts: [ { id: 'port-snow', name: 'analytics.gold_customer_360', type: 'snowflake', typeLabel: 'Snowflake Table', uri: 'snowflake://analytics_prod.dw/analytics.gold_customer_360', latency: '< 1h Batch SLA', status: 'online', }, { id: 'port-kafka', name: 'events.customer.updates.v2', type: 'kafka', typeLabel: 'Kafka Stream', uri: 'kafka://events.kafka.internal:9092/events.customer.updates.v2', latency: '< 100ms Event Time', status: 'online', }, { id: 'port-gql', name: 'GraphQL Query Port', type: 'graphql', typeLabel: 'GraphQL API', uri: 'https://api.uipkge.dev/graphql?query=customer360', latency: '< 45ms p99', status: 'online', }, { id: 'port-s3', name: 'Parquet Lake Export', type: 's3', typeLabel: 'Parquet S3 Lake', uri: 's3://lakehouse-gold/customer_360/snapshot_latest.parquet', latency: '24h Partitioned', status: 'online', }, ], tags: ['#customer360', '#revenue', '#identity', '#gold_marts', '#dbt_certified'], certifiedCompliance: 'SOC2 Type II & GDPR Art. 15 Compliant', lastUpdated: 'Aug 21, 2026', } /** * DataProductCard -- the React block, 1:1: a Data Mesh data product card with the * governance ribbon, telemetry strip, selectable output ports, a URI inspector with * copy, the owner/tags bar and Query Port / Request Access actions. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-data-product-card, [ui-data-product-card]', standalone: true, host: { class: 'block' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardHeaderComponent, UiCardDescriptionComponent, UiCardContentComponent, UiCardFooterComponent, UiProgressComponent, UiSeparatorComponent, ], template: ` <div data-slot="data-product-card" [class]="cn('w-full', className)"> <div ui-card class="border-border bg-card text-card-foreground hover:border-primary/40 relative flex flex-col overflow-hidden shadow-xs transition-colors duration-200 hover:shadow-sm" > <div ui-card-header class="space-y-3 p-5 pb-3"> <!-- Badges & Governance Ribbon --> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center gap-1.5"> <!-- Domain Tag --> <span ui-badge wrap variant="outline" class="border-border bg-muted/30 text-muted-foreground gap-1 text-xs font-normal" > <lucide-icon [img]="Layers" class="text-primary size-3" aria-hidden="true" /> <span>{{ product.domain }}</span> </span> <!-- Tier Tag --> <span ui-badge wrap variant="secondary" class="border-success/30 bg-success/10 text-success font-mono text-xs font-semibold" > <lucide-icon [img]="Award" class="size-3" aria-hidden="true" /> {{ product.tier }} </span> </div> <!-- Compliance Seal --> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" aria-hidden="true" /> <span class="hidden font-medium sm:inline">{{ product.certifiedCompliance }}</span> <span class="font-medium sm:hidden">SOC2 Certified</span> </div> </div> <!-- Title & Version Lockup --> <div class="space-y-1"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-baseline gap-2"> <h3 class="text-foreground font-mono text-base font-bold tracking-tight break-all sm:text-lg"> {{ product.name }} </h3> <span class="text-muted-foreground font-mono text-xs font-medium">{{ product.version }}</span> </div> <span ui-badge wrap variant="outline" class="text-muted-foreground gap-1 text-xs font-normal"> <lucide-icon [img]="Activity" class="text-success size-3 animate-pulse" aria-hidden="true" /> <span class="font-mono tabular-nums">{{ product.slaCompliance }}% SLA</span> </span> </div> <!-- Business Value Description --> <p ui-card-description class="text-muted-foreground text-xs leading-relaxed sm:text-sm"> {{ product.description }} </p> </div> </div> <div ui-card-content class="flex-1 space-y-4 p-5 pt-0"> <!-- 4 Key Telemetry Metrics Strip --> <div class="border-border/80 bg-muted/20 grid grid-cols-2 gap-2 rounded-lg border p-2.5 text-xs sm:grid-cols-4 sm:p-3" > <div class="space-y-0.5"> <span class="text-muted-foreground block">SLA Compliance</span> <div class="flex items-center gap-1.5"> <span class="text-success text-success font-mono text-sm font-bold tabular-nums"> {{ product.slaCompliance }}% </span> <span class="text-success font-semibold">✓</span> </div> <ui-progress [value]="product.slaCompliance" class="bg-muted h-1.5" /> </div> <div class="space-y-0.5"> <span class="text-muted-foreground block">Data Freshness</span> <span class="text-foreground block truncate font-mono text-xs font-semibold" [title]="product.freshness"> {{ product.freshness }} </span> <span class="text-muted-foreground text-xs">SLA: < 1h Target</span> </div> <div class="space-y-0.5"> <span class="text-muted-foreground block">Completeness</span> <span class="text-foreground block font-mono text-sm font-bold tabular-nums"> {{ product.completenessPct }}% </span> <span class="text-muted-foreground text-xs">0.6% Max Nulls</span> </div> <div class="space-y-0.5"> <span class="text-muted-foreground block">Active Consumers</span> <div class="flex items-center gap-1"> <lucide-icon [img]="Users" class="text-primary size-3.5" aria-hidden="true" /> <span class="text-foreground font-mono text-sm font-bold tabular-nums"> {{ product.activeConsumersCount }} Apps </span> </div> <span class="text-muted-foreground text-xs">Certified Live Ports</span> </div> </div> <!-- Multi-Modal Output Ports Selector --> <div class="space-y-2"> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold tracking-wide uppercase"> Certified Multi-Modal Output Ports ({{ product.outputPorts.length }}) </span> <span class="text-muted-foreground text-xs">Click port to inspect URI</span> </div> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> @for (port of product.outputPorts; track port.id) { <button type="button" [class]="portClass(port)" [attr.aria-pressed]="isSelected(port)" (click)="selectedPortId.set(port.id)" > <div class="flex min-w-0 items-start gap-2.5"> <div class="bg-muted text-foreground mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-md" > @switch (port.type) { @case ('snowflake') { <lucide-icon [img]="Database" class="text-info size-3.5" aria-hidden="true" /> } @case ('kafka') { <lucide-icon [img]="Radio" class="text-warning size-3.5" aria-hidden="true" /> } @case ('graphql') { <lucide-icon [img]="Server" class="text-chart-1 size-3.5" aria-hidden="true" /> } @default { <lucide-icon [img]="HardDrive" class="text-success size-3.5" aria-hidden="true" /> } } </div> <div class="min-w-0 space-y-0.5"> <div class="flex items-center gap-1.5"> <span class="text-foreground truncate text-xs font-semibold">{{ port.typeLabel }}</span> <span class="bg-success size-1.5 shrink-0 rounded-full" aria-hidden="true"></span> </div> <p class="text-muted-foreground truncate font-mono text-xs">{{ port.name }}</p> <p class="text-muted-foreground text-xs font-normal">{{ port.latency }}</p> </div> </div> <div class="shrink-0 pt-0.5 pl-1"> @if (isSelected(port)) { <span class="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full text-xs font-bold" > ✓ </span> } @else { <lucide-icon [img]="ChevronRight" class="text-muted-foreground/40 group-hover:text-foreground size-4" aria-hidden="true" /> } </div> </button> } </div> </div> <!-- Active Selected Port URI Inspector Box --> <div class="border-border bg-muted/40 flex flex-col gap-2 rounded-lg border p-3 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="min-w-0 space-y-0.5"> <span class="text-muted-foreground text-xs font-medium">Selected Endpoint URI:</span> <p class="text-foreground truncate font-mono text-xs font-medium select-all">{{ selectedPort()?.uri }}</p> </div> <div class="flex shrink-0 items-center gap-1.5 pt-1 sm:pt-0"> <button ui-button variant="outline" size="sm" class="h-7 gap-1 px-2 text-xs transition-transform duration-150 active:scale-95" (click)="copySelected()" > @if (isCopied()) { <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" /> } @else { <lucide-icon [img]="Copy" class="size-3" aria-hidden="true" /> } <span>{{ isCopied() ? 'Copied' : 'Copy URI' }}</span> </button> </div> </div> <!-- Product Owner & Tags Bar --> <div class="flex flex-wrap items-center justify-between gap-3 pt-1"> <!-- Owner Profile --> <div class="flex items-center gap-2"> <span ui-avatar class="border-border size-7 border"> <span ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-semibold"> {{ product.owner.fallback }} </span> </span> <div> <p class="text-foreground text-xs leading-none font-medium">{{ product.owner.name }}</p> <p class="text-muted-foreground text-xs">{{ product.owner.team }}</p> </div> </div> <!-- Tags --> <div class="flex flex-wrap items-center gap-1"> @for (tag of product.tags.slice(0, 3); track tag) { <span ui-badge wrap variant="outline" class="text-muted-foreground border-border/70 text-xs font-normal" > {{ tag }} </span> } </div> </div> </div> <ui-separator /> <!-- Footer Action Controls --> <div ui-card-footer class="flex flex-wrap items-center justify-between gap-2 p-4 sm:px-5"> <div class="text-muted-foreground text-xs"> Last contract verified: <span class="text-foreground font-medium">{{ product.lastUpdated }}</span> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs font-medium active:scale-[0.98]" [disabled]="isQueryingPort()" (click)="handleQueryPort()" > @if (!isQueryingPort()) { <lucide-icon [img]="Zap" class="text-warning size-3.5" aria-hidden="true" /> } @if (isQueryingPort()) { <span>Running Test Query...</span> } @else { <span>Query Port</span> } </button> <button ui-button size="sm" class="h-8 gap-1.5 text-xs font-semibold shadow-xs active:scale-[0.98]" [disabled]="accessRequested()" (click)="handleRequestAccess()" > <lucide-icon [img]="KeyRound" class="size-3.5" aria-hidden="true" /> <span>{{ accessRequested() ? 'Access Requested ✓' : 'Request Access' }}</span> </button> </div> </div> </div> </div> `, }) export class UiDataProductCardComponent implements OnDestroy { @Input() product: DataProductData = DEFAULT_DATA_PRODUCT @Input() variant: 'full' | 'compact' | 'minimal' = 'full' @Input('class') className?: string protected readonly cn = cn protected readonly Activity = Activity protected readonly Award = Award protected readonly Check = Check protected readonly ChevronRight = ChevronRight protected readonly Copy = Copy protected readonly Database = Database protected readonly HardDrive = HardDrive protected readonly KeyRound = KeyRound protected readonly Layers = Layers protected readonly Radio = Radio protected readonly Server = Server protected readonly ShieldCheck = ShieldCheck protected readonly Users = Users protected readonly Zap = Zap readonly copiedPortId = signal<string | null>(null) readonly accessRequested = signal(false) readonly isQueryingPort = signal(false) /** null until the user picks a port: React seeds it from the first port (or 'port-snow'). */ readonly selectedPortId = signal<string | null>(null) private readonly timers = new Set<ReturnType<typeof setTimeout>>() private currentPortId(): string { return this.selectedPortId() ?? this.product.outputPorts[0]?.id ?? 'port-snow' } selectedPort(): OutputPort | undefined { const id = this.currentPortId() return this.product.outputPorts.find((p) => p.id === id) ?? this.product.outputPorts[0] } isSelected(port: OutputPort): boolean { return this.currentPortId() === port.id } isCopied(): boolean { const port = this.selectedPort() return this.copiedPortId() === port?.id } portClass(port: OutputPort): string { return cn( 'group flex items-start justify-between rounded-lg border p-2.5 text-left transition-[colors,transform] duration-150 active:scale-[0.98]', this.isSelected(port) ? 'border-primary/50 bg-primary/5 ring-primary/30 ring-1' : 'border-border bg-card hover:border-primary/25 hover:bg-muted/40', ) } private later(fn: () => void, ms: number): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } copyUri(port: OutputPort): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(port.uri) this.copiedPortId.set(port.id) this.later(() => this.copiedPortId.set(null), 2000) } } copySelected(): void { const port = this.selectedPort() if (port) this.copyUri(port) } handleRequestAccess(): void { this.accessRequested.set(true) this.later(() => this.accessRequested.set(false), 4000) } handleQueryPort(): void { this.isQueryingPort.set(true) this.later(() => this.isQueryingPort.set(false), 1200) } ngOnDestroy(): void { for (const t of this.timers) clearTimeout(t) this.timers.clear() } }src/app/components/blocks/data-product-card/index.ts0.1 kB
export { UiDataProductCardComponent, DEFAULT_DATA_PRODUCT, type DataProductData, type OutputPort, } from './data-product-card.component'
Raw manifest:https://uipkge.dev/r/angular/data-product-card.json