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 shadcn-vue@latest add https://uipkge.dev/r/vue/data-product-card.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/data-product-card.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/data-product-card.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/data-product-card.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/data-product-cardInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
product | DataProductData | — | optional |
variant | 'full''compact''minimal' | 'full' | optional |
class | HTMLAttributes['class'] | — | optional |
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 (1)
app/components/blocks/DataProductCard.vue15.5 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, ref } from 'vue' import { Activity, Check, ChevronRight, Copy, Database, HardDrive, KeyRound, Layers, Radio, Server, Award, ShieldCheck, Users, Zap, } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Separator } from '@/components/ui/separator' 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 } 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', } interface Props { product?: DataProductData variant?: 'full' | 'compact' | 'minimal' class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { variant: 'full', }) const activeProduct = computed(() => props.product ?? DEFAULT_DATA_PRODUCT) const copiedPortId = ref<string | null>(null) const accessRequested = ref(false) const isQueryingPort = ref(false) const selectedPortId = ref<string>(DEFAULT_DATA_PRODUCT.outputPorts[0].id) const selectedPort = computed(() => { return ( activeProduct.value.outputPorts.find((p) => p.id === selectedPortId.value) ?? activeProduct.value.outputPorts[0] ) }) function copyUri(port: OutputPort) { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(port.uri) copiedPortId.value = port.id setTimeout(() => { if (copiedPortId.value === port.id) { copiedPortId.value = null } }, 2000) } } function handleRequestAccess() { accessRequested.value = true setTimeout(() => { accessRequested.value = false }, 4000) } function handleQueryPort() { isQueryingPort.value = true setTimeout(() => { isQueryingPort.value = false }, 1200) } </script> <template> <div data-slot="data-product-card" :class="cn('w-full', props.class)"> <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" > <CardHeader 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 --> <Badge wrap variant="outline" class="border-border bg-muted/30 text-muted-foreground gap-1 text-xs font-normal" > <Layers class="text-primary size-3" aria-hidden="true" /> <span>{{ activeProduct.domain }}</span> </Badge> <!-- Tier Tag --> <Badge wrap variant="secondary" class="border-success/30 bg-success/10 text-success font-mono text-xs font-semibold" > <Award class="size-3" aria-hidden="true" /> {{ activeProduct.tier }} </Badge> </div> <!-- Compliance Seal --> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <ShieldCheck class="text-success size-3.5" aria-hidden="true" /> <span class="hidden font-medium sm:inline">{{ activeProduct.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"> {{ activeProduct.name }} </h3> <span class="text-muted-foreground font-mono text-xs font-medium"> {{ activeProduct.version }} </span> </div> <Badge wrap variant="outline" class="text-muted-foreground gap-1 text-xs font-normal"> <Activity class="text-success size-3 animate-pulse" aria-hidden="true" /> <span class="font-mono tabular-nums">{{ activeProduct.slaCompliance }}% SLA</span> </Badge> </div> <!-- Business Value Description --> <CardDescription class="text-muted-foreground text-xs leading-relaxed sm:text-sm"> {{ activeProduct.description }} </CardDescription> </div> </CardHeader> <CardContent 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"> {{ activeProduct.slaCompliance }}% </span> <span class="text-success font-semibold">✓</span> </div> <Progress :model-value="activeProduct.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="activeProduct.freshness" > {{ activeProduct.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"> {{ activeProduct.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"> <Users class="text-primary size-3.5" aria-hidden="true" /> <span class="text-foreground font-mono text-sm font-bold tabular-nums"> {{ activeProduct.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 ({{ activeProduct.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"> <button v-for="port in activeProduct.outputPorts" :key="port.id" type="button" :class="[ 'group flex items-start justify-between rounded-lg border p-2.5 text-left transition-[colors,transform] duration-150 active:scale-[0.98]', selectedPortId === port.id ? 'border-primary/50 bg-primary/5 ring-primary/30 ring-1' : 'border-border bg-card hover:border-primary/25 hover:bg-muted/40', ]" @click="selectedPortId = 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" > <Database v-if="port.type === 'snowflake'" class="text-info size-3.5" aria-hidden="true" /> <Radio v-else-if="port.type === 'kafka'" class="text-warning size-3.5" aria-hidden="true" /> <Server v-else-if="port.type === 'graphql'" class="text-chart-1 size-3.5" aria-hidden="true" /> <HardDrive v-else 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" /> </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"> <span v-if="selectedPortId === port.id" class="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full text-xs font-bold" > ✓ </span> <ChevronRight v-else 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 variant="outline" size="sm" class="h-7 gap-1 px-2 text-xs transition-transform duration-150 active:scale-95" @click="copyUri(selectedPort)" > <Check v-if="copiedPortId === selectedPort.id" class="text-success size-3" aria-hidden="true" /> <Copy v-else class="size-3" aria-hidden="true" /> <span>{{ copiedPortId === selectedPort.id ? '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"> <Avatar class="border-border size-7 border"> <AvatarFallback class="bg-primary/10 text-primary text-xs font-semibold"> {{ activeProduct.owner.fallback }} </AvatarFallback> </Avatar> <div> <p class="text-foreground text-xs leading-none font-medium"> {{ activeProduct.owner.name }} </p> <p class="text-muted-foreground text-xs"> {{ activeProduct.owner.team }} </p> </div> </div> <!-- Tags --> <div class="flex flex-wrap items-center gap-1"> <Badge wrap v-for="tag in activeProduct.tags.slice(0, 3)" :key="tag" variant="outline" class="text-muted-foreground border-border/70 text-xs font-normal" > {{ tag }} </Badge> </div> </div> </CardContent> <Separator /> <!-- Footer Action Controls --> <CardFooter 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">{{ activeProduct.lastUpdated }}</span> </div> <div class="flex items-center gap-2"> <Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs font-medium active:scale-[0.98]" :disabled="isQueryingPort" @click="handleQueryPort" > <Zap v-if="!isQueryingPort" class="text-warning size-3.5" aria-hidden="true" /> <span v-if="isQueryingPort">Running Test Query...</span> <span v-else>Query Port</span> </Button> <Button size="sm" class="h-8 gap-1.5 text-xs font-semibold shadow-xs active:scale-[0.98]" :disabled="accessRequested" @click="handleRequestAccess" > <KeyRound class="size-3.5" aria-hidden="true" /> <span>{{ accessRequested ? 'Access Requested ✓' : 'Request Access' }}</span> </Button> </div> </CardFooter> </Card> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/data-product-card.json