Feature Store Registry
blockaiFeast 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
$pnpm dlx uipkge-ng@latest add feature-store-registry$npx uipkge-ng@latest add feature-store-registry$yarn dlx uipkge-ng@latest add feature-store-registry$bunx uipkge-ng@latest add feature-store-registryManifest:
https://uipkge.dev/r/angular/feature-store-registry.jsonInstalls to:src/app/components/blocks/feature-store-registry/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.
FeatureDefinitioninterface FeatureDefinition {
name: string
dtype: 'Float64' | 'Int32' | 'String'
description: string
aggregationWindow: string
onlineCacheKey: string
freshness: string
status: 'Online & Batch Ready'
sourceSql: string
sampleValue: string
}MetricCardinterface 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, '&').replace(/</g, '<').replace(/>/g, '>') } /** One source line as highlighted HTML (spans with Tailwind colour classes only). */ export function highlightPythonLine(line: string): string { if (!line.trim()) return ' ' 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 · < 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 & 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 & Type</th> <th ui-table-head class="text-xs font-semibold">Description & 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">< 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 & 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 & 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">< 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
export { UiFeatureStoreRegistryComponent, highlightPythonLine, type FeatureDefinition, type MetricCard, type FeatureStoreRegistryTab, type FeatureStoreRegistryTypeFilter, } from './feature-store-registry.component'
Raw manifest:https://uipkge.dev/r/angular/feature-store-registry.json