Menu

Change language

GitHub repo

Data Mesh Domain Catalog

blockdevops

Zhamak Dehghani Data Mesh architecture domain catalog, Data Products directory, polyglot output ports (Snowflake, Kafka, GraphQL, S3), and federated computational governance SLA scorecards.

Installation

$npx uipkge-ng@latest add data-mesh-domain-catalog
Manifest:https://uipkge.dev/r/angular/data-mesh-domain-catalog.jsonInstalls to:src/app/components/blocks/data-mesh-domain-catalog/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

IconSpec
interface IconSpec {
  icon: LucideIconData
  class: string
}
DataMeshCopyEvent
interface DataMeshCopyEvent {
  text: string
  identifier: string
}
DomainMeta
interface DomainMeta {
  id: string
  name: string
  lead: string
  role: string
  productsCount: number
  slaScore: number
  activeConsumers: number
  storageVolume: string
  throughput: string
  tier: string
  description: string
  iconName: 'CreditCard' | 'Users' | 'Truck' | 'Activity'
}
OutputPortSchemaColumn
interface OutputPortSchemaColumn {
  name: string
  type: string
  nullable: boolean
  isKey?: boolean
  desc: string
  constraints?: string
}
OutputPortDetails
interface OutputPortDetails {
  id: DataMeshPortId
  name: string
  typeLabel: string
  badgeVariant?: 'default' | 'secondary' | 'outline'
  protocol: string
  uri: string
  accessRole: string
  schemaRegistry?: string
  format?: string
  syncCadence?: string
  throughput?: string
  rateLimit?: string
  columns?: OutputPortSchemaColumn[]
  sampleQuery?: string
  samplePayload?: string
  mockResults?: Array<Record<string, string | number | boolean>>
}
DataProductItem
interface DataProductItem {
  id: string
  name: string
  displayName: string
  domainId: string
  domainName: string
  version: string
  tier: string
  classification: 'PII-Confidential' | 'Financial-Audit' | 'Internal-Ops' | 'Platform-Internal'
  description: string
  contractHash: string
  owner: {
    name: string
    role: string
    handle: string
    team: string
    initials: string
    avatar?: string
  }
  slaScore: number
  freshnessLatency: string
  uptime: string
  assertionsPassed: number
  assertionsTotal: number
  activeConsumersCount: number
  topConsumers: string[]
  ports: OutputPortDetails[]
  governancePolicies: string[]
}

Files installed (7)

  • src/app/components/blocks/data-mesh-domain-catalog/data-mesh-domain-catalog.component.ts25.9 kB
    import { Component, DestroyRef, Input, OnInit, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core'
    import {
      Activity,
      Copy,
      CreditCard,
      Database,
      Eye,
      FileCode2,
      HardDrive,
      KeyRound,
      Layers,
      LucideAngularModule,
      MoreHorizontal,
      Network,
      Plus,
      Search,
      ShieldCheck,
      Table2,
      Terminal,
      Truck,
      Users,
      Zap,
      type LucideIconData,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import { UiDataMeshProductDetailComponent } from './data-mesh-product-detail.component'
    import { UiDataMeshQueryRunnerComponent } from './data-mesh-query-runner.component'
    import { UiDataMeshModalsComponent } from './data-mesh-modals.component'
    import { DOMAINS, DATA_PRODUCTS } from './data-mesh-data'
    import type {
      DomainMeta,
      OutputPortSchemaColumn,
      OutputPortDetails,
      DataProductItem,
      DataMeshPortId,
    } from './data-mesh-types'
    
    export type { DomainMeta, OutputPortSchemaColumn, OutputPortDetails, DataProductItem, DataMeshPortId }
    
    interface IconSpec {
      icon: LucideIconData
      class: string
    }
    
    const DOMAIN_ICONS: Record<DomainMeta['iconName'], IconSpec> = {
      CreditCard: { icon: CreditCard, class: 'text-chart-1 size-4' },
      Users: { icon: Users, class: 'text-info size-4' },
      Truck: { icon: Truck, class: 'text-warning size-4' },
      Activity: { icon: Activity, class: 'text-success size-4' },
    }
    
    const PORT_BADGE_ICONS: Record<DataMeshPortId, IconSpec> = {
      snowflake: { icon: Table2, class: 'text-info size-3' },
      kafka: { icon: Zap, class: 'text-warning size-3' },
      graphql: { icon: Network, class: 'size-3 text-pink-400' },
      s3: { icon: HardDrive, class: 'text-success size-3' },
    }
    
    /**
     * Data Mesh domain catalog: domain node cards, the data products table, the selected
     * product's output-port inspector, an inline query runner and the access / register modals.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-data-mesh-domain-catalog, [ui-data-mesh-domain-catalog]',
      standalone: true,
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiProgressComponent,
        UiTableComponent,
        UiTableHeaderComponent,
        UiTableBodyComponent,
        UiTableRowComponent,
        UiTableHeadComponent,
        UiTableCellComponent,
        UiDataMeshProductDetailComponent,
        UiDataMeshQueryRunnerComponent,
        UiDataMeshModalsComponent,
      ],
      template: `
        <div data-slot="data-mesh-domain-catalog" [class]="rootClass">
          <!-- Modals: Request Access & Register Product (fixed overlays; first so the last visible section keeps no bottom margin) -->
          <ui-data-mesh-modals
            [accessModalOpen]="isAccessModalOpen()"
            (accessModalOpenChange)="isAccessModalOpen.set($event)"
            [registerModalOpen]="isRegisterModalOpen()"
            (registerModalOpenChange)="isRegisterModalOpen.set($event)"
            [product]="selectedProduct()"
            [domains]="domains"
          />
    
          <!-- Header Section -->
          <header class="border-border bg-card/70 rounded-xl border p-4 sm:p-5">
            <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
              <div class="space-y-2">
                <!-- Top Row: Org Breadcrumb & Governance Status -->
                <div class="flex flex-wrap items-center gap-2 text-xs">
                  <div class="bg-primary/10 text-primary flex size-6 items-center justify-center rounded-md">
                    <lucide-icon [img]="Network" class="size-3.5" />
                  </div>
                  <span class="text-foreground font-semibold">UIPKGE Global Data Mesh</span>
                  <span class="text-muted-foreground font-mono">4 Core Domains</span>
                  <div
                    class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 font-medium"
                  >
                    <span class="relative flex size-2">
                      <span class="bg-success absolute inline-flex size-full rounded-full opacity-75"></span>
                      <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                    </span>
                    <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" />
                    <span>Federated Computational Governance Active</span>
                  </div>
                </div>
    
                <!-- Main Title -->
                <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                  Data Mesh Domain Catalog &amp; Data Products Directory
                </h1>
                <p class="text-muted-foreground max-w-3xl text-xs leading-relaxed sm:text-sm">
                  Zhamak Dehghani architectural architecture for decentralized domain data ownership, discoverable polyglot
                  output ports, and automated computational governance SLA scorecards.
                </p>
              </div>
    
              <!-- Header Action Button -->
              <div class="flex shrink-0 items-center gap-2 pt-1 lg:pt-0">
                <button
                  ui-button
                  variant="default"
                  size="sm"
                  class="h-9 gap-1.5 text-xs font-medium"
                  (click)="isRegisterModalOpen.set(true)"
                >
                  <lucide-icon [img]="Plus" class="size-3.5" />
                  <span>Register New Data Product</span>
                </button>
              </div>
            </div>
          </header>
    
          <!-- 4 Domain Summary Cards -->
          <section class="space-y-3">
            <div class="flex items-center justify-between gap-x-2 px-1">
              <div class="flex items-center gap-2">
                <lucide-icon [img]="Layers" class="text-primary size-4" />
                <h2 class="text-foreground text-xs font-semibold tracking-wider uppercase">
                  Decentralized Domain Nodes (4)
                </h2>
              </div>
              <span class="text-muted-foreground text-xs">Click a domain card to filter Data Products</span>
            </div>
    
            <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
              @for (domain of domains; track domain.id) {
                <div [class]="domainCardClass(domain.id)" (click)="selectDomain(domain.id)">
                  <!-- Domain Card Header -->
                  <div class="flex items-start justify-between gap-2">
                    <div class="flex items-center gap-2.5">
                      <div
                        class="bg-muted border-border/80 flex size-8 shrink-0 items-center justify-center rounded-lg border"
                      >
                        <lucide-icon [img]="domainIcon(domain).icon" [class]="domainIcon(domain).class" />
                      </div>
                      <div>
                        <h3 class="text-foreground leading-tight font-semibold">{{ domain.name }}</h3>
                        <span class="text-muted-foreground text-xs">{{ domain.productsCount }} Data Products</span>
                      </div>
                    </div>
    
                    <span
                      ui-badge
                      [variant]="selectedDomainFilter() === domain.id ? 'default' : 'secondary'"
                      class="shrink-0 font-mono text-xs"
                    >
                      {{ selectedDomainFilter() === domain.id ? 'Filtered' : domain.slaScore + '% SLA' }}
                    </span>
                  </div>
    
                  <!-- Domain Description -->
                  <p class="text-muted-foreground line-clamp-2 leading-relaxed">{{ domain.description }}</p>
    
                  <!-- Domain Meta & Operational Metrics -->
                  <div class="border-border/60 grid grid-cols-2 gap-2 border-t pt-2.5 text-xs">
                    <div>
                      <span class="text-muted-foreground block">Storage Vol</span>
                      <span class="text-foreground font-mono font-medium">{{ domain.storageVolume }}</span>
                    </div>
                    <div>
                      <span class="text-muted-foreground block">Throughput</span>
                      <span class="text-foreground font-mono font-medium">{{ domain.throughput }}</span>
                    </div>
                  </div>
    
                  <!-- Domain Lead Info -->
                  <div class="bg-muted/50 flex items-center justify-between gap-x-2 rounded-lg px-2.5 py-1.5">
                    <div class="flex items-center gap-1.5 overflow-hidden">
                      <ui-avatar class="size-4.5 shrink-0">
                        <ui-avatar-fallback class="text-xs">{{ initialsOf(domain.lead) }}</ui-avatar-fallback>
                      </ui-avatar>
                      <span class="text-foreground truncate font-medium">{{ domain.lead }}</span>
                    </div>
                    <span class="text-muted-foreground shrink-0 font-mono text-xs">
                      {{ domain.activeConsumers }} clients
                    </span>
                  </div>
                </div>
              }
            </div>
          </section>
    
          <!-- Data Products Table Section -->
          <section class="border-border bg-card space-y-4 rounded-xl border p-4 shadow-xs sm:p-5">
            <!-- Toolbar: Title, Search, Port Type Selector -->
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <div class="space-y-0.5">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="Database" class="text-primary size-4" />
                  <h2 class="text-foreground text-sm font-semibold">Data Products Catalog</h2>
                  <span ui-badge variant="outline" class="font-mono text-xs">
                    {{ filteredDataProducts().length }} / {{ dataProducts.length }}
                  </span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Autonomous product quantum nodes adhering to computational policies and explicit input/output schemas.
                </p>
              </div>
    
              <div class="flex flex-wrap items-center gap-2">
                <!-- Port Filter Selector -->
                <div class="flex items-center gap-1.5">
                  <span class="text-muted-foreground text-xs font-medium">Output:</span>
                  <select
                    (change)="selectedPortTypeFilter.set(valueOf($event))"
                    class="border-border bg-background text-foreground focus-visible:ring-ring h-8 rounded-md border px-2.5 text-xs outline-none focus-visible:ring-1"
                  >
                    @for (opt of portFilterOptions; track opt.value) {
                      <option [value]="opt.value" [selected]="selectedPortTypeFilter() === opt.value">
                        {{ opt.label }}
                      </option>
                    }
                  </select>
                </div>
    
                <!-- Search Input -->
                <div class="relative w-full sm:w-56">
                  <lucide-icon
                    [img]="Search"
                    class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                  />
                  <input
                    [value]="searchQuery()"
                    (input)="searchQuery.set(valueOf($event))"
                    placeholder="Search product, owner, domain..."
                    class="border-border bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border pr-3 pl-8 text-xs outline-none focus-visible:ring-1"
                  />
                </div>
              </div>
            </div>
    
            <!-- Main Data Products Table -->
            <div class="border-border overflow-x-auto rounded-lg border">
              <div ui-table>
                <thead ui-table-header>
                  <tr ui-table-row class="bg-muted/40">
                    <th ui-table-head class="text-xs">Data Product &amp; Contract</th>
                    <th ui-table-head class="text-xs">Domain Node</th>
                    <th ui-table-head class="text-xs">Freshness SLA</th>
                    <th ui-table-head class="text-xs">Active Output Ports</th>
                    <th ui-table-head class="text-xs">Consumers &amp; Traffic</th>
                    <th ui-table-head class="text-right text-xs">Actions</th>
                  </tr>
                </thead>
                <tbody ui-table-body>
                  @for (product of filteredDataProducts(); track product.id) {
                    <tr ui-table-row [class]="rowClass(product.id)" (click)="selectProduct(product)">
                      <!-- Name & Contract -->
                      <td ui-table-cell class="py-3">
                        <div class="space-y-1">
                          <div class="flex items-center gap-2">
                            <span class="text-foreground font-semibold">{{ product.displayName }}</span>
                            <span ui-badge variant="outline" class="font-mono text-xs">v{{ product.version }}</span>
                          </div>
                          <div class="flex items-center gap-1.5 text-xs">
                            <code class="text-muted-foreground font-mono">{{ product.name }}</code>
                            <span class="text-muted-foreground/40">·</span>
                            <span ui-badge [variant]="classificationVariant(product)" class="text-xs">
                              {{ product.classification }}
                            </span>
                          </div>
                        </div>
                      </td>
    
                      <!-- Domain Node & Owner -->
                      <td ui-table-cell class="py-3">
                        <div class="space-y-1">
                          <span class="text-foreground text-xs font-medium">{{ product.domainName }}</span>
                          <div class="flex items-center gap-1.5">
                            <ui-avatar class="size-4 shrink-0">
                              <ui-avatar-fallback class="text-xs">{{ product.owner.initials }}</ui-avatar-fallback>
                            </ui-avatar>
                            <span class="text-muted-foreground text-xs">{{ product.owner.name }}</span>
                          </div>
                        </div>
                      </td>
    
                      <!-- SLA & Quality Telemetry -->
                      <td ui-table-cell class="py-3">
                        <div class="w-36 space-y-1.5">
                          <div class="flex items-center justify-between text-xs">
                            <span class="text-muted-foreground font-mono">SLA Score</span>
                            <span class="text-success font-mono font-semibold">{{ product.slaScore }}%</span>
                          </div>
                          <ui-progress [value]="product.slaScore" class="h-1.5" />
                          <div class="text-muted-foreground flex items-center justify-between font-mono text-xs">
                            <span>Latency: {{ product.freshnessLatency }}</span>
                            <span>{{ product.assertionsPassed }}/{{ product.assertionsTotal }} Checks</span>
                          </div>
                        </div>
                      </td>
    
                      <!-- Active Output Ports Badges -->
                      <td ui-table-cell class="py-3">
                        <div class="flex flex-wrap gap-1.5">
                          @for (port of product.ports; track port.id) {
                            <span
                              ui-badge
                              [variant]="
                                port.id === activePortId() && selectedProductId() === product.id ? 'default' : 'secondary'
                              "
                              class="h-6 gap-1 font-mono text-xs"
                            >
                              <lucide-icon [img]="portBadgeIcon(port.id).icon" [class]="portBadgeIcon(port.id).class" />
                              <span>{{ port.id.toUpperCase() }}</span>
                            </span>
                          }
                        </div>
                      </td>
    
                      <!-- Consumers & Volume -->
                      <td ui-table-cell class="py-3">
                        <div class="space-y-0.5 text-xs">
                          <div class="flex items-center gap-1 text-xs">
                            <lucide-icon [img]="Users" class="text-muted-foreground size-3" />
                            <span class="text-foreground font-semibold">{{ product.activeConsumersCount }}</span>
                            <span class="text-muted-foreground">registered apps</span>
                          </div>
                          <p class="text-muted-foreground truncate font-mono text-xs">
                            {{ product.topConsumers.slice(0, 2).join(', ') }}
                          </p>
                        </div>
                      </td>
    
                      <!-- Row Action Dropdown -->
                      <td ui-table-cell class="py-3 text-right" (click)="$event.stopPropagation()">
                        <ui-dropdown-menu>
                          <button ui-button ui-dropdown-menu-trigger variant="ghost" size="sm" class="size-8 p-0">
                            <lucide-icon [img]="MoreHorizontal" class="size-4" />
                          </button>
                          <ui-dropdown-menu-content align="end" class="w-48 text-xs">
                            <ui-dropdown-menu-label class="text-xs">Product Actions</ui-dropdown-menu-label>
                            <ui-dropdown-menu-item class="cursor-pointer gap-2" (select)="selectProduct(product)">
                              <lucide-icon [img]="Eye" class="size-3.5" />
                              <span>View Specifications</span>
                            </ui-dropdown-menu-item>
                            <ui-dropdown-menu-item class="cursor-pointer gap-2" (select)="handleOpenQueryRunner(product)">
                              <lucide-icon [img]="Terminal" class="text-primary size-3.5" />
                              <span>Interactive Query Runner</span>
                            </ui-dropdown-menu-item>
                            <ui-dropdown-menu-item class="cursor-pointer gap-2" (select)="handleOpenAccessModal(product)">
                              <lucide-icon [img]="KeyRound" class="text-warning size-3.5" />
                              <span>Request Consumer Access</span>
                            </ui-dropdown-menu-item>
                            <ui-dropdown-menu-separator />
                            <ui-dropdown-menu-item
                              class="cursor-pointer gap-2"
                              (select)="copyToClipboard(product.ports[0]?.uri || '', product.id)"
                            >
                              <lucide-icon [img]="Copy" class="size-3.5" />
                              <span>Copy Primary Port URI</span>
                            </ui-dropdown-menu-item>
                            <ui-dropdown-menu-item
                              class="cursor-pointer gap-2"
                              (select)="copyToClipboard(product.contractHash, product.contractHash)"
                            >
                              <lucide-icon [img]="FileCode2" class="text-chart-2 size-3.5" />
                              <span>Copy Contract Hash</span>
                            </ui-dropdown-menu-item>
                          </ui-dropdown-menu-content>
                        </ui-dropdown-menu>
                      </td>
                    </tr>
                  }
                </tbody>
              </div>
            </div>
          </section>
    
          <!-- Data Product Output Port Architecture & Governance Card (Selected Product) -->
          <section
            ui-data-mesh-product-detail
            [product]="selectedProduct()"
            [port]="activePort()"
            [activePortId]="activePortId()"
            [copiedKey]="copiedUri()"
            (portChange)="activePortId.set($event)"
            (copyText)="copyToClipboard($event.text, $event.identifier)"
            (requestAccess)="handleOpenAccessModal($event)"
            (runQuery)="handleOpenQueryRunner($event)"
          ></section>
    
          <!-- Interactive Drawer / Modal: Query Output Port -->
          @if (isQueryDrawerOpen()) {
            <div
              ui-data-mesh-query-runner
              [product]="selectedProduct()"
              [port]="activePort()"
              [isSimulatingQuery]="isSimulatingQuery()"
              (closed)="isQueryDrawerOpen.set(false)"
              (runQuery)="runSimulatedQuery()"
            ></div>
          }
        </div>
      `,
    })
    export class UiDataMeshDomainCatalogComponent implements OnInit {
      @Input('class') className?: string
      @Input() initialSelectedProductId = 'dp_customer_360_profile'
      @Input() initialDomainFilter = 'all'
    
      readonly domains: DomainMeta[] = DOMAINS
      readonly dataProducts: DataProductItem[] = DATA_PRODUCTS
    
      readonly portFilterOptions = [
        { value: 'all', label: 'All Ports' },
        { value: 'snowflake', label: 'Snowflake View' },
        { value: 'kafka', label: 'Kafka Stream' },
        { value: 'graphql', label: 'GraphQL Mesh' },
        { value: 's3', label: 'S3 Delta Lake' },
      ]
    
      readonly selectedDomainFilter = signal('all')
      readonly selectedPortTypeFilter = signal('all')
      readonly searchQuery = signal('')
      readonly selectedProductId = signal('dp_customer_360_profile')
      readonly activePortId = signal<DataMeshPortId>('snowflake')
      readonly copiedUri = signal<string | null>(null)
    
      // Modals & drawer state
      readonly isAccessModalOpen = signal(false)
      readonly isRegisterModalOpen = signal(false)
      readonly isQueryDrawerOpen = signal(false)
      readonly isSimulatingQuery = signal(false)
    
      readonly selectedProduct = computed<DataProductItem>(
        () => this.dataProducts.find((p) => p.id === this.selectedProductId()) || this.dataProducts[0]!,
      )
    
      readonly activePort = computed<OutputPortDetails>(() => {
        const ports = this.selectedProduct().ports
        return ports.find((p) => p.id === this.activePortId()) || ports[0]!
      })
    
      readonly filteredDataProducts = computed(() => {
        const domain = this.selectedDomainFilter()
        const portType = this.selectedPortTypeFilter()
        const query = this.searchQuery()
        return this.dataProducts.filter((product) => {
          if (domain !== 'all' && product.domainId !== domain) return false
          if (portType !== 'all' && !product.ports.some((p) => p.id === portType)) return false
          if (query.trim()) {
            const q = query.toLowerCase()
            const matches = [product.name, product.displayName, product.domainName, product.owner.name, product.description]
              .map((s) => s.toLowerCase())
              .some((s) => s.includes(q))
            if (!matches) return false
          }
          return true
        })
      })
    
      private readonly timers = new Set<ReturnType<typeof setTimeout>>()
    
      constructor() {
        inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t)))
      }
    
      ngOnInit(): void {
        this.selectedDomainFilter.set(this.initialDomainFilter)
        this.selectedProductId.set(this.initialSelectedProductId)
      }
    
      get rootClass(): string {
        return cn(
          'bg-background text-foreground border-border w-full space-y-6 rounded-xl border p-4 shadow-xs sm:p-6',
          this.className,
        )
      }
    
      domainCardClass(domainId: string): string {
        return cn(
          'group relative cursor-pointer space-y-3 rounded-xl border p-4 text-xs shadow-xs transition-colors',
          this.selectedDomainFilter() === domainId
            ? 'border-primary bg-primary/5 ring-primary/30 ring-1'
            : 'border-border bg-card hover:border-border/80 hover:bg-muted/30',
        )
      }
    
      rowClass(productId: string): string {
        return cn(
          'hover:bg-muted/50 cursor-pointer transition-colors',
          this.selectedProductId() === productId && 'bg-primary/5 font-medium',
        )
      }
    
      domainIcon(domain: DomainMeta): IconSpec {
        return DOMAIN_ICONS[domain.iconName]
      }
    
      portBadgeIcon(id: DataMeshPortId): IconSpec {
        return PORT_BADGE_ICONS[id]
      }
    
      classificationVariant(product: DataProductItem): BadgeVariant {
        if (product.classification === 'PII-Confidential') return 'destructive'
        if (product.classification === 'Financial-Audit') return 'default'
        return 'secondary'
      }
    
      initialsOf(name: string): string {
        return name
          .split(' ')
          .map((n) => n[0])
          .join('')
      }
    
      valueOf(event: Event): string {
        return (event.target as HTMLInputElement | HTMLSelectElement).value
      }
    
      selectDomain(domainId: string): void {
        this.selectedDomainFilter.set(this.selectedDomainFilter() === domainId ? 'all' : domainId)
      }
    
      selectProduct(product: DataProductItem): void {
        this.selectedProductId.set(product.id)
        const first = product.ports[0]
        if (first) this.activePortId.set(first.id)
      }
    
      copyToClipboard(text: string, identifier: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(text).catch(() => {})
          this.copiedUri.set(identifier)
          this.later(() => this.copiedUri.set(null), 2000)
        }
      }
    
      runSimulatedQuery(): void {
        this.isSimulatingQuery.set(true)
        this.later(() => this.isSimulatingQuery.set(false), 1000)
      }
    
      handleOpenAccessModal(product: DataProductItem): void {
        this.selectProduct(product)
        this.isAccessModalOpen.set(true)
      }
    
      handleOpenQueryRunner(product: DataProductItem): void {
        this.selectProduct(product)
        this.isQueryDrawerOpen.set(true)
      }
    
      private later(fn: () => void, ms: number): void {
        const t = setTimeout(() => {
          this.timers.delete(t)
          fn()
        }, ms)
        this.timers.add(t)
      }
    
      protected readonly Copy = Copy
      protected readonly Database = Database
      protected readonly Eye = Eye
      protected readonly FileCode2 = FileCode2
      protected readonly KeyRound = KeyRound
      protected readonly Layers = Layers
      protected readonly MoreHorizontal = MoreHorizontal
      protected readonly Network = Network
      protected readonly Plus = Plus
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Terminal = Terminal
      protected readonly Users = Users
    }
    
  • src/app/components/blocks/data-mesh-domain-catalog/data-mesh-product-detail.component.ts18 kB
  • src/app/components/blocks/data-mesh-domain-catalog/data-mesh-query-runner.component.ts4.5 kB
  • src/app/components/blocks/data-mesh-domain-catalog/data-mesh-modals.component.ts15.4 kB
  • src/app/components/blocks/data-mesh-domain-catalog/data-mesh-types.ts1.6 kB
  • src/app/components/blocks/data-mesh-domain-catalog/data-mesh-data.ts24.7 kB
  • src/app/components/blocks/data-mesh-domain-catalog/index.ts0.6 kB

Raw manifest:https://uipkge.dev/r/angular/data-mesh-domain-catalog.json