Menu

Change language

GitHub repo

Feature Store Registry

blockai

Feast and Tecton style machine learning feature store registry with entity definitions, low-latency Redis online cache status, Snowflake batch source lineage, feature schema inspection table, throughput metrics, and Python Feast feature view code generator.

Installation

$npx uipkge-ng@latest add feature-store-registry
Manifest:https://uipkge.dev/r/angular/feature-store-registry.jsonInstalls to:src/app/components/blocks/feature-store-registry/

Variants

Loading interactive previews…

Schema

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

FeatureDefinition
interface FeatureDefinition {
  name: string
  dtype: 'Float64' | 'Int32' | 'String'
  description: string
  aggregationWindow: string
  onlineCacheKey: string
  freshness: string
  status: 'Online & Batch Ready'
  sourceSql: string
  sampleValue: string
}
MetricCard
interface MetricCard {
  title: string
  value: string
  subtitle: string
  badge: string
  icon: LucideIconData
}

Files installed (2)

  • src/app/components/blocks/feature-store-registry/feature-store-registry.component.ts37.9 kB
    import { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core'
    import {
      Activity,
      Check,
      CheckCircle2,
      Clock,
      Code2,
      Copy,
      Cpu,
      Database,
      Download,
      Fingerprint,
      Key,
      Layers,
      Loader2,
      LucideAngularModule,
      Search,
      ShieldCheck,
      Table2,
      Workflow,
      Zap,
      type LucideIconData,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.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 FeatureDefinition {
      name: string
      dtype: 'Float64' | 'Int32' | 'String'
      description: string
      aggregationWindow: string
      onlineCacheKey: string
      freshness: string
      status: 'Online & Batch Ready'
      sourceSql: string
      sampleValue: string
    }
    
    export interface MetricCard {
      title: string
      value: string
      subtitle: string
      badge: string
      icon: LucideIconData
    }
    
    export type FeatureStoreRegistryTab = 'features' | 'python-feast' | 'lineage'
    export type FeatureStoreRegistryTypeFilter = 'all' | 'Float64' | 'Int32' | 'String'
    
    const metrics: MetricCard[] = [
      {
        title: 'Total Features Registered',
        value: '18 Active Features',
        subtitle: '5 in this view · 4 views active',
        badge: '18 in Registry',
        icon: Layers,
      },
      {
        title: 'Online Cache Hit Rate',
        value: '99.8%',
        subtitle: '1.4ms p95 latency · Redis Cluster',
        badge: '< 2ms SLA',
        icon: Zap,
      },
      {
        title: 'Daily Ingestion Throughput',
        value: '14.8M Updates / Day',
        subtitle: '171.2 req/s avg · Stream + Batch',
        badge: 'Real-time CDC',
        icon: Activity,
      },
      {
        title: 'Max Point-in-Time TTL',
        value: '30 Days',
        subtitle: 'Lookback window · Anti-leakage safe',
        badge: 'AS-OF Safe',
        icon: Clock,
      },
    ]
    
    const features: FeatureDefinition[] = [
      {
        name: 'avg_transaction_value_30d',
        dtype: 'Float64',
        description: '30-day sliding average transaction amount in base USD currency',
        aggregationWindow: '30-day sliding mean',
        onlineCacheKey: 'user:ft:avg_tx_val_30d',
        freshness: 'Synced 4m ago',
        status: 'Online & Batch Ready',
        sourceSql: 'AVG(amount_usd) OVER (PARTITION BY user_id ROWS BETWEEN 30 PRECEDING AND CURRENT ROW)',
        sampleValue: '$342.80',
      },
      {
        name: 'failed_logins_24h',
        dtype: 'Int32',
        description: 'Failed authentication attempts in rolling 24-hour security window',
        aggregationWindow: '24-hour count',
        onlineCacheKey: 'user:ft:fail_login_24h',
        freshness: 'Synced 2m ago',
        status: 'Online & Batch Ready',
        sourceSql: 'SUM(CASE WHEN auth_success = FALSE THEN 1 ELSE 0 END)',
        sampleValue: '0',
      },
      {
        name: 'lifetime_chargeback_count',
        dtype: 'Int32',
        description: 'Total historical dispute and payment chargeback occurrences',
        aggregationWindow: 'Cumulative lifetime',
        onlineCacheKey: 'user:ft:cb_cnt_lt',
        freshness: 'Synced 12m ago',
        status: 'Online & Batch Ready',
        sourceSql: 'COUNT(chargeback_id) OVER (PARTITION BY user_id)',
        sampleValue: '0',
      },
      {
        name: 'preferred_payment_method',
        dtype: 'String',
        description: 'Most frequently utilized payment method category across checkout sessions',
        aggregationWindow: 'Mode category',
        onlineCacheKey: 'user:ft:pref_pay_method',
        freshness: 'Synced 18m ago',
        status: 'Online & Batch Ready',
        sourceSql: 'MODE(payment_method_code) OVER (PARTITION BY user_id)',
        sampleValue: '"apple_pay"',
      },
      {
        name: 'risk_score_ml_v3',
        dtype: 'Float64',
        description: 'Real-time XGBoost inference fraud risk score probability [0.0 - 1.0]',
        aggregationWindow: 'Real-time inference',
        onlineCacheKey: 'user:ft:risk_score_v3',
        freshness: 'Synced 1m ago',
        status: 'Online & Batch Ready',
        sourceSql: 'MODEL_PREDICT_FRAUD_V3(features)',
        sampleValue: '0.042',
      },
    ]
    
    const pythonFeastSource = `from datetime import timedelta
    from feast import (
        BatchSource,
        Entity,
        FeatureView,
        Field,
        SnowflakeSource,
        ValueType,
    )
    from feast.types import Float64, Int32, String
    
    # 1. Define Primary Entity
    user_entity = Entity(
        name="user_id",
        value_type=ValueType.STRING,
        join_keys=["user_id"],
        description="Global UUID representing verified consumer and business accounts",
    )
    
    # 2. Snowflake Gold DW Batch Offline Source
    transactions_batch_source = SnowflakeSource(
        database="ANALYTICS_PROD",
        schema="GOLD_MARTS",
        table="fct_user_transactions_daily",
        timestamp_field="event_timestamp",
        created_timestamp_column="created_at",
    )
    
    # 3. Dual-Store Feature View Definition (Redis Online + Snowflake Batch)
    user_30d_transaction_aggregates = FeatureView(
        name="user_30d_transaction_aggregates",
        entities=[user_entity],
        ttl=timedelta(days=30),
        schema=[
            Field(
                name="avg_transaction_value_30d",
                dtype=Float64,
                description="30-day sliding average transaction amount in base USD currency",
            ),
            Field(
                name="failed_logins_24h",
                dtype=Int32,
                description="Failed authentication attempts in rolling 24-hour security window",
            ),
            Field(
                name="lifetime_chargeback_count",
                dtype=Int32,
                description="Total historical dispute and payment chargeback occurrences",
            ),
            Field(
                name="preferred_payment_method",
                dtype=String,
                description="Most frequently utilized payment method category across checkout sessions",
            ),
            Field(
                name="risk_score_ml_v3",
                dtype=Float64,
                description="Real-time XGBoost inference fraud risk score probability [0.0 - 1.0]",
            ),
        ],
        online=True,
        source=transactions_batch_source,
        tags={
            "team": "fraud-ml",
            "tier": "tier-1-prod",
            "online_store": "redis_cluster",
            "batch_store": "snowflake_gold",
            "sla": "sub-2ms",
        },
    )`
    
    function escapeHtml(str: string): string {
      return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    }
    
    /** One source line as highlighted HTML (spans with Tailwind colour classes only). */
    export function highlightPythonLine(line: string): string {
      if (!line.trim()) return '&nbsp;'
      const escaped = escapeHtml(line)
    
      // Comments
      if (/^\s*#/.test(escaped)) {
        return `<span class="text-neutral-500 italic">${escaped}</span>`
      }
    
      // Each emitted <span> is parked behind a letter-only placeholder so a later
      // pass cannot match inside markup an earlier pass produced.
      const parked: string[] = []
      const park = (html: string) => {
        const key = String(parked.length)
          .split('')
          .map((d) => String.fromCharCode(97 + Number(d)))
          .join('')
        parked.push(html)
        return `\u0000${key}\u0000`
      }
    
      let res = escaped
    
      // Strings
      res = res.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) =>
        park(`<span class="text-success font-normal">${match}</span>`),
      )
    
      // Keywords
      res = res.replace(
        /\b(from|import|def|class|return|as|True|False|None|with|for|in|and|or|not|is|if|else|elif)\b/g,
        (m) => park(`<span class="text-chart-1 font-semibold">${m}</span>`),
      )
    
      // Types / Feast Classes
      res = res.replace(
        /\b(Entity|FeatureView|Field|SnowflakeSource|BatchSource|ValueType|Float64|Int32|String|timedelta)\b/g,
        (m) => park(`<span class="text-warning font-medium">${m}</span>`),
      )
    
      // Arguments
      res = res.replace(
        /\b(name|value_type|join_keys|description|database|schema|table|timestamp_field|created_timestamp_column|entities|ttl|dtype|online|source|tags|days)\b(?=\s*=)/g,
        (m) => park(`<span class="text-info">${m}</span>`),
      )
    
      // Numbers
      res = res.replace(/\b(\d+)\b/g, (m) => park(`<span class="text-warning font-medium">${m}</span>`))
    
      // Restore every parked span
      return res.replace(/\u0000([a-j]+)\u0000/g, (_, key: string) => {
        const idx = Number(
          key
            .split('')
            .map((c: string) => String(c.charCodeAt(0) - 97))
            .join(''),
        )
        return parked[idx] ?? ''
      })
    }
    
    /**
     * FeatureStoreRegistry -- the React block, 1:1: a Feast / Tecton style feature view page with
     * entity + store pills, "Generate Training Dataset" / "Materialize Online Features" actions,
     * four metric cards, and three tabs: a searchable, type-filterable feature table (click a cache
     * key to copy it) with an online vector lookup strip, the highlighted Python Feast definition
     * (copyable), and the offline / online storage lineage cards. Edit the data consts above.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-feature-store-registry, [ui-feature-store-registry]',
      standalone: true,
      // React renders the root <div> itself: keep this wrapper out of the layout.
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiInputComponent,
        UiTableComponent,
        UiTableHeaderComponent,
        UiTableBodyComponent,
        UiTableRowComponent,
        UiTableHeadComponent,
        UiTableCellComponent,
        UiTabsComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
        UiTabsContentComponent,
      ],
      template: `
        <div data-slot="feature-store-registry" [class]="rootClass">
          <!-- TOP FEATURE VIEW HEADER -->
          <header class="border-border bg-card/70 border-b px-4 py-4 sm:px-6">
            <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
              <!-- Left: Title, Entity, Stores -->
              <div class="flex flex-wrap items-center gap-3">
                <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-lg shadow-xs">
                  <lucide-icon [img]="Cpu" class="size-5" />
                </div>
    
                <div class="flex flex-col gap-1">
                  <div class="flex flex-wrap items-center gap-2">
                    <span class="text-muted-foreground font-mono text-xs">feature_view /</span>
                    <h1 class="text-foreground font-mono text-base font-bold tracking-tight break-all sm:text-lg">
                      user_30d_transaction_aggregates
                    </h1>
                    <span ui-badge wrap variant="outline" class="font-mono text-xs font-normal">v2.4.0</span>
                  </div>
    
                  <!-- Metadata Badges Strip -->
                  <div class="flex flex-wrap items-center gap-2 pt-0.5">
                    <!-- Entity Pill -->
                    <div
                      class="border-border bg-muted/40 text-foreground inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 font-mono text-xs"
                    >
                      <lucide-icon [img]="Key" class="text-primary size-3" />
                      <span class="text-muted-foreground">Entity:</span>
                      <span class="font-semibold">user_id</span>
                      <span class="text-muted-foreground">[UUID]</span>
                    </div>
    
                    <!-- Online Store Pill -->
                    <div
                      class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 font-mono text-xs font-medium"
                    >
                      <span class="bg-success size-1.5 animate-pulse rounded-full"></span>
                      <span>Redis Cluster · &lt; 2ms latency</span>
                    </div>
    
                    <!-- Batch Store Pill -->
                    <div
                      class="border-info/30 bg-info/10 text-info inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 font-mono text-xs font-medium"
                    >
                      <lucide-icon [img]="Database" class="text-info size-3" />
                      <span>Snowflake Gold DW</span>
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- Right: Action Buttons -->
              <div class="flex flex-wrap items-center gap-2">
                <!-- Generate Training Dataset Button -->
                <button
                  ui-button
                  aria-label="Download attachment"
                  variant="outline"
                  size="sm"
                  class="h-9 gap-1.5 text-xs font-medium shadow-none"
                  [disabled]="isGeneratingDataset()"
                  (click)="handleGenerateDataset()"
                >
                  @if (isGeneratingDataset()) {
                    <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" />
                  } @else if (datasetSuccess()) {
                    <lucide-icon [img]="Check" class="text-success size-3.5" />
                  } @else {
                    <lucide-icon [img]="Download" class="size-3.5" />
                  }
                  <span>{{ datasetLabel() }}</span>
                </button>
    
                <!-- Materialize Online Features Button -->
                <button
                  ui-button
                  variant="default"
                  size="sm"
                  class="bg-primary text-primary-foreground hover:bg-primary/90 h-9 gap-1.5 text-xs font-semibold shadow-xs"
                  [disabled]="isMaterializing()"
                  (click)="handleMaterialize()"
                >
                  @if (isMaterializing()) {
                    <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" />
                  } @else if (materializeSuccess()) {
                    <lucide-icon [img]="Check" class="text-success size-3.5" />
                  } @else {
                    <lucide-icon [img]="Zap" class="size-3.5 fill-current" />
                  }
                  <span>{{ materializeLabel() }}</span>
                </button>
              </div>
            </div>
          </header>
    
          <!-- 4 FEATURE STORE METRIC CARDS -->
          <div class="border-border bg-muted/15 border-b p-4 sm:p-6">
            <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
              @for (metric of metrics; track metric.title) {
                <ui-card class="border-border bg-card hover:bg-muted/30 shadow-none transition-colors">
                  <ui-card-content class="flex flex-col justify-between gap-3 p-4">
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground text-xs font-medium">{{ metric.title }}</span>
                      <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="metric.icon" class="size-3.5" />
                      </div>
                    </div>
    
                    <div>
                      <div class="text-foreground font-mono text-xl font-bold tracking-tight">{{ metric.value }}</div>
                      <div class="flex items-center justify-between pt-1 text-xs">
                        <span class="text-muted-foreground truncate">{{ metric.subtitle }}</span>
                        <span ui-badge wrap variant="secondary" class="ml-1 shrink-0 font-mono text-xs font-normal">{{
                          metric.badge
                        }}</span>
                      </div>
                    </div>
                  </ui-card-content>
                </ui-card>
              }
            </div>
          </div>
    
          <!-- MAIN TABS STUDIO -->
          <div class="flex flex-1 flex-col">
            <ui-tabs [value]="activeTab()" (valueChange)="setActiveTab($event)" class="flex flex-1 flex-col">
              <!-- Tabs Bar -->
              <div
                class="border-border bg-card flex flex-wrap items-center justify-between gap-3 border-b px-4 py-2.5 sm:px-6"
              >
                <div ui-tabs-list class="grid h-9 grid-cols-3">
                  <button ui-tabs-trigger value="features" class="gap-1.5 text-xs">
                    <lucide-icon [img]="Table2" class="text-primary size-3.5" />
                    <span>Feature Definitions</span>
                    <span ui-badge wrap variant="secondary" class="ml-1 h-4 px-1 font-mono text-xs">{{
                      features.length
                    }}</span>
                  </button>
                  <button ui-tabs-trigger value="python-feast" class="gap-1.5 text-xs">
                    <lucide-icon [img]="Code2" class="text-warning size-3.5" />
                    <span>Feast Definition</span>
                  </button>
                  <button ui-tabs-trigger value="lineage" class="gap-1.5 text-xs">
                    <lucide-icon [img]="Workflow" class="text-info size-3.5" />
                    <span>Lineage &amp; Architecture</span>
                  </button>
                </div>
    
                <div class="flex items-center gap-2">
                  <span ui-badge wrap variant="outline" class="text-success gap-1 font-mono text-xs">
                    <lucide-icon [img]="ShieldCheck" class="size-3" />
                    <span>Redis Online Serving: ACTIVE</span>
                  </span>
                </div>
              </div>
    
              <!-- TAB 1: FEATURE DEFINITIONS TABLE -->
              <ui-tabs-content value="features" class="m-0 flex flex-1 flex-col space-y-5 p-4 sm:p-6">
                <!-- Filter / Search Controls -->
                <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                  <div class="flex flex-wrap items-center gap-2">
                    <div class="relative w-full sm:w-72">
                      <!-- [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]="searchQuery()"
                        (valueChange)="searchQuery.set($event)"
                        placeholder="Filter by feature name, type, or cache key..."
                        class="h-8 pl-8 font-mono text-xs"
                      />
                    </div>
    
                    <!-- Type filter pills -->
                    <div class="flex items-center gap-1">
                      @for (t of typeFilters; track t) {
                        <button type="button" [class]="typePillClass(t)" (click)="selectedType.set(t)">
                          {{ t === 'all' ? 'All Types' : t }}
                        </button>
                      }
                    </div>
                  </div>
    
                  <div class="text-muted-foreground flex items-center gap-2 font-mono text-xs">
                    <span>Showing {{ filteredFeatures().length }} of {{ features.length }} features</span>
                  </div>
                </div>
    
                <!-- Feature Definitions Table -->
                <ui-card class="border-border overflow-hidden border shadow-none">
                  <div class="overflow-x-auto">
                    <div ui-table>
                      <thead ui-table-header>
                        <tr ui-table-row class="bg-muted/40 hover:bg-muted/40">
                          <th ui-table-head class="text-xs font-semibold">Feature Name &amp; Type</th>
                          <th ui-table-head class="text-xs font-semibold">Description &amp; Aggregation Window</th>
                          <th ui-table-head class="text-xs font-semibold">Online Cache Key (Redis)</th>
                          <th ui-table-head class="text-xs font-semibold">Freshness</th>
                          <th ui-table-head class="text-xs font-semibold">Status</th>
                        </tr>
                      </thead>
                      <tbody ui-table-body>
                        @for (feature of filteredFeatures(); track feature.name) {
                          <tr ui-table-row class="text-xs">
                            <!-- Feature Name & Type -->
                            <td ui-table-cell class="font-mono">
                              <div class="space-y-1">
                                <div class="text-foreground font-semibold tracking-tight">{{ feature.name }}</div>
                                <span ui-badge variant="secondary" [class]="dtypeBadgeClass(feature.dtype)">{{
                                  feature.dtype
                                }}</span>
                              </div>
                            </td>
    
                            <!-- Description & Aggregation Window -->
                            <td ui-table-cell class="max-w-[280px]">
                              <div class="space-y-1">
                                <p class="text-muted-foreground text-xs leading-relaxed">{{ feature.description }}</p>
                                <span ui-badge wrap variant="outline" class="gap-1 font-mono text-xs font-normal">
                                  <lucide-icon [img]="Clock" class="text-muted-foreground size-2.5" />
                                  {{ feature.aggregationWindow }}
                                </span>
                              </div>
                            </td>
    
                            <!-- Online Cache Key -->
                            <td ui-table-cell>
                              <button
                                type="button"
                                class="border-border/80 bg-muted/60 hover:bg-muted hover:border-primary/40 text-foreground group inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 font-mono text-xs transition-colors"
                                title="Click to copy Redis key"
                                (click)="copyToClipboard(feature.onlineCacheKey, 'key', feature.name)"
                              >
                                <span class="text-primary font-semibold">GET</span>
                                <span>{{ feature.onlineCacheKey }}</span>
                                @if (copiedKey() === feature.name) {
                                  <lucide-icon [img]="Check" class="text-success size-3 transition-transform" />
                                } @else {
                                  <lucide-icon
                                    [img]="Copy"
                                    class="text-muted-foreground size-3 opacity-60 group-hover:opacity-100"
                                  />
                                }
                              </button>
                            </td>
    
                            <!-- Freshness / Last Synced -->
                            <td ui-table-cell class="text-muted-foreground font-mono whitespace-nowrap">
                              <div class="flex items-center gap-1.5">
                                <lucide-icon [img]="Clock" class="text-success size-3" />
                                <span>{{ feature.freshness }}</span>
                              </div>
                            </td>
    
                            <!-- Feature Status -->
                            <td ui-table-cell class="whitespace-nowrap">
                              <span
                                ui-badge
                                wrap
                                variant="outline"
                                class="border-success/30 bg-success/10 text-success gap-1.5 font-mono text-xs font-medium"
                              >
                                <span class="bg-success size-1.5 rounded-full"></span>
                                {{ feature.status }}
                              </span>
                            </td>
                          </tr>
                        }
                      </tbody>
                    </div>
                  </div>
                </ui-card>
    
                <!-- Interactive Entity Live Feature Vector Inspector -->
                <ui-card class="border-border bg-card/60 space-y-4 border p-4 shadow-none sm:p-5">
                  <div
                    class="border-border/80 flex flex-col gap-2 border-b pb-3 sm:flex-row sm:items-center sm:justify-between"
                  >
                    <div class="flex flex-wrap items-center gap-2">
                      <lucide-icon [img]="Fingerprint" class="text-primary size-4 shrink-0" />
                      <span class="text-foreground text-xs font-semibold">Live Online Feature Vector Lookup Simulator</span>
                      <span ui-badge wrap variant="secondary" class="font-mono text-xs">feast.get_online_features()</span>
                    </div>
                    <div class="flex items-center gap-2">
                      <span class="text-muted-foreground font-mono text-xs">Target Entity:</span>
                      <span class="text-foreground font-mono text-xs font-semibold">{{ selectedEntityId }}</span>
                    </div>
                  </div>
    
                  <div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-5">
                    @for (feature of features; track feature.name) {
                      <div class="border-border/70 bg-muted/25 flex flex-col justify-between rounded-lg border p-3">
                        <div class="space-y-1">
                          <span class="text-muted-foreground block truncate font-mono text-xs font-medium">{{
                            feature.name
                          }}</span>
                          <span class="text-muted-foreground text-xs">{{ feature.aggregationWindow }}</span>
                        </div>
                        <div
                          class="border-border/40 mt-3 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 border-t pt-2"
                        >
                          <span class="text-foreground font-mono text-sm font-bold">{{ feature.sampleValue }}</span>
                          <span class="text-success font-mono text-xs">&lt; 1.2ms</span>
                        </div>
                      </div>
                    }
                  </div>
                </ui-card>
              </ui-tabs-content>
    
              <!-- TAB 2: PYTHON FEAST DEFINITION CODE BOX -->
              <ui-tabs-content value="python-feast" class="m-0 flex flex-1 flex-col space-y-4 p-4 sm:p-6">
                <div class="border-border bg-muted/40 overflow-hidden rounded-xl border">
                  <!-- Code Box Toolbar Header -->
                  <div class="border-border bg-card flex items-center justify-between border-b px-4 py-2.5">
                    <div class="flex items-center gap-2">
                      <div class="bg-warning/10 text-warning flex size-6 items-center justify-center rounded">
                        <lucide-icon [img]="Code2" class="size-3.5" />
                      </div>
                      <span class="text-foreground font-mono text-xs font-semibold"
                        >feature_definitions/user_aggregates.py</span
                      >
                      <span ui-badge wrap variant="secondary" class="font-mono text-xs">Python 3.11 · Feast 0.42+</span>
                    </div>
    
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      class="h-7 gap-1.5 text-xs font-medium"
                      (click)="copyToClipboard(pythonFeastSource, 'code')"
                    >
                      @if (copiedFeast()) {
                        <lucide-icon [img]="Check" class="text-success size-3" />
                      } @else {
                        <lucide-icon [img]="Copy" class="size-3" />
                      }
                      <span>{{ copiedFeast() ? 'Copied Feast Code!' : 'Copy Python Code' }}</span>
                    </button>
                  </div>
    
                  <!-- Code Gutter and Syntax Highlighter -->
                  <div
                    class="relative flex overflow-x-auto bg-neutral-950 font-mono text-xs leading-relaxed text-neutral-100 dark:bg-neutral-950"
                  >
                    <!-- Line Numbers Gutter -->
                    <div
                      class="w-11 shrink-0 overflow-hidden border-r border-neutral-800 bg-neutral-900/60 py-3 pr-2 text-right text-neutral-500 select-none"
                    >
                      @for (line of pythonLines; track $index) {
                        <div>{{ $index + 1 }}</div>
                      }
                    </div>
    
                    <!-- Python Code Content -->
                    <div class="flex-1 overflow-x-auto p-3 whitespace-pre select-text">
                      @for (html of highlightedLines; track $index) {
                        <div class="h-5 font-mono leading-5" [innerHTML]="html"></div>
                      }
                    </div>
                  </div>
    
                  <!-- Code Footer Meta -->
                  <div
                    class="border-border/60 bg-muted/20 text-muted-foreground flex items-center justify-between border-t px-4 py-1.5 font-mono text-xs"
                  >
                    <div class="flex items-center gap-3">
                      <span>{{ pythonLines.length }} lines</span>
                      <span>·</span>
                      <span>UTF-8</span>
                      <span>·</span>
                      <span>Feast Entity &amp; FeatureView DSL</span>
                    </div>
                    <div class="text-success flex items-center gap-1.5">
                      <lucide-icon [img]="CheckCircle2" class="size-3.5" />
                      <span>Valid Feast Schema Contract</span>
                    </div>
                  </div>
                </div>
              </ui-tabs-content>
    
              <!-- TAB 3: STORAGE & LINEAGE ARCHITECTURE -->
              <ui-tabs-content value="lineage" class="m-0 flex flex-1 flex-col space-y-4 p-4 sm:p-6">
                <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
                  <!-- Offline Batch Lineage Card -->
                  <ui-card class="border-border bg-card space-y-3 border p-5 shadow-none">
                    <div class="border-border flex items-center justify-between border-b pb-2.5">
                      <div class="flex items-center gap-2">
                        <lucide-icon [img]="Database" class="text-info size-4" />
                        <span class="text-foreground text-xs font-semibold">Offline Batch Store Architecture</span>
                      </div>
                      <span ui-badge wrap variant="secondary" class="font-mono text-xs">Snowflake Gold</span>
                    </div>
    
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      Immutable historical logs for time-travel queries, training set generation, and AS-OF joins without
                      point-in-time data leakage.
                    </p>
    
                    <div class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-3 font-mono text-xs">
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Database &amp; Schema:</span>
                        <span class="text-foreground font-semibold">ANALYTICS_PROD.GOLD_MARTS</span>
                      </div>
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Source Table:</span>
                        <span class="text-foreground font-semibold">fct_user_transactions_daily</span>
                      </div>
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Timestamp Partition:</span>
                        <span class="text-foreground">event_timestamp (UTC)</span>
                      </div>
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Historical Depth:</span>
                        <span class="text-foreground">3 Years (Lookback TTL 30 Days)</span>
                      </div>
                    </div>
                  </ui-card>
    
                  <!-- Online Low-Latency Cache Card -->
                  <ui-card class="border-border bg-card space-y-3 border p-5 shadow-none">
                    <div class="border-border flex items-center justify-between border-b pb-2.5">
                      <div class="flex items-center gap-2">
                        <lucide-icon [img]="Zap" class="text-success size-4" />
                        <span class="text-foreground text-xs font-semibold">Online Key-Value Cache Architecture</span>
                      </div>
                      <span ui-badge wrap variant="outline" class="border-success/30 text-success font-mono text-xs">
                        Redis 7.2 Cluster
                      </span>
                    </div>
    
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      Ultra-low latency memory tier providing sub-2 millisecond retrieval for online model inference
                      microservices and fraud scoring.
                    </p>
    
                    <div class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-3 font-mono text-xs">
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Cluster Topology:</span>
                        <span class="text-foreground font-semibold">3 Master Shards + 3 Replicas</span>
                      </div>
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Key Prefix Pattern:</span>
                        <span class="text-foreground font-semibold">user:ft:{{ '{' }}feature_name{{ '}' }}</span>
                      </div>
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Read SLA (p95):</span>
                        <span class="text-success font-semibold">&lt; 1.4ms Latency</span>
                      </div>
                      <div class="flex justify-between">
                        <span class="text-muted-foreground">Materialization Schedule:</span>
                        <span class="text-foreground">Every 15 mins via Feast Job</span>
                      </div>
                    </div>
                  </ui-card>
                </div>
              </ui-tabs-content>
            </ui-tabs>
          </div>
        </div>
      `,
    })
    export class UiFeatureStoreRegistryComponent {
      @Input('class') className?: string
    
      protected readonly metrics = metrics
      protected readonly features = features
      protected readonly pythonFeastSource = pythonFeastSource
      protected readonly pythonLines = pythonFeastSource.split('\n')
      protected readonly highlightedLines = this.pythonLines.map(highlightPythonLine)
      protected readonly typeFilters: readonly FeatureStoreRegistryTypeFilter[] = ['all', 'Float64', 'Int32', 'String']
      protected readonly selectedEntityId = 'usr_9a4f201d-7e2b-4d68'
    
      readonly activeTab = signal<FeatureStoreRegistryTab>('features')
      readonly searchQuery = signal('')
      readonly selectedType = signal<FeatureStoreRegistryTypeFilter>('all')
      readonly isMaterializing = signal(false)
      readonly materializeSuccess = signal(false)
      readonly isGeneratingDataset = signal(false)
      readonly datasetSuccess = signal(false)
      readonly copiedFeast = signal(false)
      readonly copiedKey = signal<string | null>(null)
    
      readonly filteredFeatures = computed(() => {
        const query = this.searchQuery().trim().toLowerCase()
        const type = this.selectedType()
        return features.filter((f) => {
          const matchesSearch =
            !query ||
            f.name.toLowerCase().includes(query) ||
            f.description.toLowerCase().includes(query) ||
            f.onlineCacheKey.toLowerCase().includes(query) ||
            f.dtype.toLowerCase().includes(query)
          const matchesType = type === 'all' || f.dtype === type
          return matchesSearch && matchesType
        })
      })
    
      protected get rootClass(): string {
        return cn(
          'bg-background text-foreground border-border flex w-full flex-col overflow-hidden rounded-xl border shadow-xs',
          this.className,
        )
      }
    
      protected readonly datasetLabel = computed(() =>
        this.isGeneratingDataset()
          ? 'Building Parquet...'
          : this.datasetSuccess()
            ? 'Dataset Exported (14.2 MB)'
            : 'Generate Training Dataset',
      )
    
      protected readonly materializeLabel = computed(() =>
        this.isMaterializing()
          ? 'Syncing to Redis...'
          : this.materializeSuccess()
            ? 'Materialized 18.4k Keys!'
            : 'Materialize Online Features',
      )
    
      protected readonly Activity = Activity
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Code2 = Code2
      protected readonly Copy = Copy
      protected readonly Cpu = Cpu
      protected readonly Database = Database
      protected readonly Download = Download
      protected readonly Fingerprint = Fingerprint
      protected readonly Key = Key
      protected readonly Loader2 = Loader2
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Table2 = Table2
      protected readonly Workflow = Workflow
      protected readonly Zap = Zap
    
      private readonly timers = new Set<ReturnType<typeof setTimeout>>()
    
      constructor() {
        inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t)))
      }
    
      setActiveTab(value: string): void {
        if (value === 'features' || value === 'python-feast' || value === 'lineage') this.activeTab.set(value)
      }
    
      typePillClass(t: FeatureStoreRegistryTypeFilter): string {
        return cn(
          'cursor-pointer rounded-md px-2 py-1 font-mono text-xs transition-colors',
          this.selectedType() === t
            ? 'bg-primary text-primary-foreground font-medium shadow-xs'
            : 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground',
        )
      }
    
      dtypeBadgeClass(dtype: FeatureDefinition['dtype']): string {
        return cn(
          'font-mono text-xs font-normal',
          dtype === 'Float64' && 'border-warning/20 bg-warning/10 text-warning border',
          dtype === 'Int32' && 'border-info/20 bg-info/10 text-info border',
          dtype === 'String' && 'border-chart-1/20 bg-chart-1/10 text-chart-1 border',
        )
      }
    
      copyToClipboard(text: string, type: 'code' | 'key', keyName?: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(text).catch(() => {})
          if (type === 'code') {
            this.copiedFeast.set(true)
            this.later(2000, () => this.copiedFeast.set(false))
          } else if (type === 'key' && keyName) {
            this.copiedKey.set(keyName)
            this.later(2000, () => this.copiedKey.set(null))
          }
        }
      }
    
      handleMaterialize(): void {
        if (this.isMaterializing()) return
        this.isMaterializing.set(true)
        this.materializeSuccess.set(false)
        this.later(1200, () => {
          this.isMaterializing.set(false)
          this.materializeSuccess.set(true)
          this.later(3000, () => this.materializeSuccess.set(false))
        })
      }
    
      handleGenerateDataset(): void {
        if (this.isGeneratingDataset()) return
        this.isGeneratingDataset.set(true)
        this.datasetSuccess.set(false)
        this.later(1000, () => {
          this.isGeneratingDataset.set(false)
          this.datasetSuccess.set(true)
          this.later(3000, () => this.datasetSuccess.set(false))
        })
      }
    
      private later(ms: number, fn: () => void): void {
        const t = setTimeout(() => {
          this.timers.delete(t)
          fn()
        }, ms)
        this.timers.add(t)
      }
    }
    
  • src/app/components/blocks/feature-store-registry/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/feature-store-registry.json