Feature Flags Manager
blockdevopsLaunchDarkly and PostHog style feature flags management dashboard: top header with search and create flag button, summary KPI metric cards, master status switch, percentage rollout indicators, targeting rules summary, environment statuses, and action menus.
Installation
$pnpm dlx uipkge-ng@latest add feature-flags-manager$npx uipkge-ng@latest add feature-flags-manager$yarn dlx uipkge-ng@latest add feature-flags-manager$bunx uipkge-ng@latest add feature-flags-managerManifest:
https://uipkge.dev/r/angular/feature-flags-manager.jsonInstalls to:src/app/components/blocks/feature-flags-manager/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.
EnvironmentStateinterface EnvironmentState {
production: boolean
staging: boolean
development: boolean
}FeatureFlaginterface FeatureFlag {
id: string
name: string
key: string
description: string
tags: string[]
enabled: boolean
rollout: number
targetingRules: string
environments: EnvironmentState
evaluationCount?: string
lastUpdated?: string
}EnvBadgeinterface EnvBadge {
label: string
key: keyof EnvironmentState
}npm dependencies
Files installed (2)
src/app/components/blocks/feature-flags-manager/feature-flags-manager.component.ts22.1 kB
// Feature flags management dashboard: header with create button, KPI metric cards, a search + // status filter toolbar, and a flag table with master switches, rollout progress, targeting // rules, environment badges and a per-row actions menu. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { Check, Copy, Flag, History, LucideAngularModule, MoreHorizontal, Plus, Search, ShieldAlert, Sliders, SlidersHorizontal, Trash2, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent } from '@/ui/card/card.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface EnvironmentState { production: boolean staging: boolean development: boolean } export interface FeatureFlag { id: string name: string key: string description: string tags: string[] enabled: boolean rollout: number targetingRules: string environments: EnvironmentState evaluationCount?: string lastUpdated?: string } export type FeatureFlagStatusFilter = 'all' | 'enabled' | 'disabled' export const defaultFlags: FeatureFlag[] = [ { id: 'ff-1', name: 'New Checkout Experience', key: 'new-checkout-v2', description: 'Modern multi-step checkout flow with one-click payment options.', tags: ['Frontend', 'Beta', 'Core'], enabled: true, rollout: 50, targetingRules: "plan === 'enterprise' && country === 'US'", environments: { production: true, staging: true, development: false }, evaluationCount: '1.8M/day', lastUpdated: '10 min ago', }, { id: 'ff-2', name: 'Dark Mode V2 Beta', key: 'dark-mode-beta', description: 'High-contrast OKLCH dark palette with custom system theme sync.', tags: ['Frontend', 'Beta'], enabled: true, rollout: 100, targetingRules: 'beta_tester === true', environments: { production: true, staging: true, development: true }, evaluationCount: '920K/day', lastUpdated: '2 hours ago', }, { id: 'ff-3', name: 'AI Auto Complete', key: 'ai-auto-complete', description: 'LLM-assisted syntax suggestion and context-aware auto-completion.', tags: ['AI', 'Core', 'Beta'], enabled: true, rollout: 25, targetingRules: "org_tier IN ['growth', 'enterprise']", environments: { production: true, staging: true, development: false }, evaluationCount: '640K/day', lastUpdated: '1 day ago', }, { id: 'ff-4', name: 'GraphQL API v2 Gateway', key: 'graphql-api-v2', description: 'High-throughput GraphQL federation engine replacing legacy REST proxy.', tags: ['Backend', 'Core'], enabled: false, rollout: 0, targetingRules: 'internal_team === true', environments: { production: false, staging: true, development: true }, evaluationCount: '480K/day', lastUpdated: '3 days ago', }, { id: 'ff-5', name: 'Crypto Payments Pilot', key: 'crypto-payments', description: 'USDC and Ethereum settlement rail for international enterprise invoices.', tags: ['Core', 'Beta'], enabled: false, rollout: 10, targetingRules: "country IN ['US', 'DE', 'SG'] && plan === 'enterprise'", environments: { production: false, staging: true, development: false }, evaluationCount: '120K/day', lastUpdated: '5 days ago', }, ] interface EnvBadge { label: string key: keyof EnvironmentState } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-feature-flags-manager, [ui-feature-flags-manager]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiProgressComponent, UiSwitchComponent, UiTableComponent, UiTableBodyComponent, UiTableCellComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="feature-flags-manager" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div> <h2 class="text-foreground text-2xl font-bold tracking-tight">Feature Flags</h2> <p class="text-muted-foreground mt-1 text-sm"> Safely deploy features with percentage rollouts, user targeting, and kill switches. </p> </div> <div class="flex items-center gap-2"> <button ui-button> <lucide-icon [img]="Plus" aria-hidden="true" class="size-4" /> Create Flag </button> </div> </div> <!-- Summary Metric Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="p-4 shadow-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground text-xs font-medium">Total Flags</span> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Flag" aria-hidden="true" class="size-4" /> </div> </div> <div class="mt-2"> <div class="text-foreground text-2xl font-bold tracking-tight">24</div> <p class="text-muted-foreground mt-1 text-xs">5 in current scope</p> </div> </div> <div ui-card class="p-4 shadow-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground text-xs font-medium">Active Rollouts</span> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="SlidersHorizontal" aria-hidden="true" class="size-4" /> </div> </div> <div class="mt-2"> <div class="text-foreground text-2xl font-bold tracking-tight">8</div> <p class="text-muted-foreground mt-1 text-xs">Gradual traffic ramps</p> </div> </div> <div ui-card class="p-4 shadow-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground text-xs font-medium">Kill-Switches Armed</span> <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="ShieldAlert" aria-hidden="true" class="size-4" /> </div> </div> <div class="mt-2"> <div class="text-foreground text-2xl font-bold tracking-tight">3</div> <p class="text-muted-foreground mt-1 text-xs">Instant emergency trip</p> </div> </div> <div ui-card class="p-4 shadow-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground text-xs font-medium">Evaluation Volume</span> <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Zap" aria-hidden="true" class="size-4" /> </div> </div> <div class="mt-2"> <div class="text-foreground text-2xl font-bold tracking-tight">4.2M/day</div> <p class="text-muted-foreground mt-1 text-xs">99.99% cache hit · 1.2ms p99</p> </div> </div> </div> <!-- Filter & Search Toolbar --> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-1 flex-wrap items-center gap-2"> <div class="relative w-full sm:w-80"> <lucide-icon [img]="Search" aria-hidden="true" class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" placeholder="Search flags by name, key, or tag..." class="h-9 pl-8.5 text-xs sm:text-sm" /> </div> <div class="border-border bg-muted/30 flex items-center rounded-lg border p-0.5"> @for (opt of statusOptions; track opt.value) { <button type="button" [class]="statusButtonClass(opt.value)" (click)="selectedStatus.set(opt.value)"> {{ opt.label }} </button> } </div> </div> <div class="flex items-center gap-2"> <span class="text-muted-foreground text-xs tabular-nums"> Showing {{ filteredFlags().length }} of {{ flags().length }} flags </span> </div> </div> <!-- Feature Flags Table --> <div class="bg-card border-border overflow-hidden rounded-xl border shadow-xs"> <div class="overflow-x-auto"> <div ui-table class="max-w-[920px] min-w-full"> <thead ui-table-header> <tr ui-table-row class="bg-muted/40 hover:bg-transparent"> <th ui-table-head class="w-[300px]">Flag</th> <th ui-table-head class="w-[130px]">Status</th> <th ui-table-head class="w-[160px]">Rollout</th> <th ui-table-head class="min-w-[240px]">Targeting Rules</th> <th ui-table-head class="w-[160px]">Environments</th> <th ui-table-head class="w-[50px] text-right"> <span class="sr-only">Actions</span> </th> </tr> </thead> <tbody ui-table-body> @for (flag of filteredFlags(); track flag.id) { <tr ui-table-row [class]="rowClass(flag)"> <!-- Flag Name, Key, Description & Tags --> <td ui-table-cell class="py-3.5 align-top"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-semibold">{{ flag.name }}</span> </div> <div class="flex items-center gap-1.5"> <code class="bg-muted/70 text-muted-foreground border-border/50 rounded border px-1.5 py-0.5 font-mono text-xs select-all" >{{ flag.key }}</code > <button type="button" class="text-muted-foreground hover:text-foreground hover:bg-muted inline-flex min-h-6 min-w-6 items-center justify-center rounded p-0.5 transition-colors" [attr.title]="copiedKey() === flag.key ? 'Copied!' : 'Copy key'" (click)="copyFlagKey(flag.key)" > @if (copiedKey() === flag.key) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span class="sr-only">Copy {{ flag.key }}</span> </button> </div> <p class="text-muted-foreground line-clamp-1 pt-0.5 text-xs">{{ flag.description }}</p> <div class="flex flex-wrap items-center gap-1 pt-1"> @for (tag of flag.tags; track tag) { <span ui-badge variant="outline" class="text-muted-foreground px-1.5 py-0 text-xs font-normal" >{{ tag }}</span > } </div> </div> </td> <!-- Master Status Switch --> <td ui-table-cell class="py-3.5 align-middle"> <div class="flex items-center gap-2.5"> <button ui-switch [checked]="flag.enabled" [attr.aria-label]="'Toggle ' + flag.name" (checkedChange)="toggleFlag(flag.id)" ></button> <span [class]="statusTextClass(flag)">{{ flag.enabled ? 'Enabled' : 'Disabled' }}</span> </div> </td> <!-- Rollout % Visual Progress Bar --> <td ui-table-cell class="py-3.5 align-middle"> <div class="max-w-[130px] space-y-1.5"> <div class="flex items-center justify-between gap-x-2 text-xs"> <span class="text-foreground font-semibold tabular-nums">{{ flag.enabled ? flag.rollout + '%' : '0%' }}</span> <span class="text-muted-foreground text-xs">{{ rolloutLabel(flag) }}</span> </div> <div ui-progress [value]="flag.enabled ? flag.rollout : 0" class="h-1.5"></div> </div> </td> <!-- Targeting Rules Summary --> <td ui-table-cell class="py-3.5 align-middle"> <div class="flex items-center"> <code class="border-border/60 bg-muted/40 text-foreground/80 inline-flex max-w-[260px] items-center gap-1 truncate rounded border px-2 py-1 font-mono text-xs" [attr.title]="flag.targetingRules" > <span class="text-muted-foreground font-sans text-xs">Target:</span> <span class="truncate">{{ flag.targetingRules }}</span> </code> </div> </td> <!-- Environment Badges --> <td ui-table-cell class="py-3.5 align-middle"> <div class="flex items-center gap-1"> @for (env of envBadges; track env.key) { <span [class]="envBadgeClass(flag, env.key)"> <span [class]="envDotClass(flag, env.key)"></span> {{ env.label }} </span> } </div> </td> <!-- Actions Dropdown --> <td ui-table-cell class="py-3.5 text-right align-middle"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground" > <lucide-icon [img]="MoreHorizontal" aria-hidden="true" class="size-4" /> <span class="sr-only">Actions for {{ flag.name }}</span> </button> <ui-dropdown-menu-content align="end"> <ui-dropdown-menu-item> <lucide-icon [img]="Sliders" aria-hidden="true" class="mr-2 size-4" /> Edit rules </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="History" aria-hidden="true" class="mr-2 size-4" /> Audit history </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="copyFlagKey(flag.key)"> <lucide-icon [img]="Copy" aria-hidden="true" class="mr-2 size-4" /> Copy flag key </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="text-destructive focus:text-destructive" (select)="deleteFlag(flag.id)" > <lucide-icon [img]="Trash2" aria-hidden="true" class="mr-2 size-4" /> Delete flag </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } <!-- Empty State --> @if (filteredFlags().length === 0) { <tr ui-table-row> <td ui-table-cell colspan="6" class="text-muted-foreground h-36 text-center"> <div class="flex flex-col items-center justify-center space-y-1.5"> <lucide-icon [img]="Search" class="text-muted-foreground/40 size-6" /> <p class="text-foreground text-sm font-medium">No feature flags found</p> <p class="text-muted-foreground text-xs"> No flags match your search or filter criteria. Try adjusting your query. </p> </div> </td> </tr> } </tbody> </div> </div> </div> </div> `, }) export class UiFeatureFlagsManagerComponent { protected readonly Check = Check protected readonly Copy = Copy protected readonly Flag = Flag protected readonly History = History protected readonly MoreHorizontal = MoreHorizontal protected readonly Plus = Plus protected readonly Search = Search protected readonly ShieldAlert = ShieldAlert protected readonly Sliders = Sliders protected readonly SlidersHorizontal = SlidersHorizontal protected readonly Trash2 = Trash2 protected readonly Zap = Zap protected readonly statusOptions: { value: FeatureFlagStatusFilter; label: string }[] = [ { value: 'all', label: 'All' }, { value: 'enabled', label: 'Enabled' }, { value: 'disabled', label: 'Disabled' }, ] protected readonly envBadges: EnvBadge[] = [ { label: 'Prod', key: 'production' }, { label: 'Stg', key: 'staging' }, { label: 'Dev', key: 'development' }, ] private readonly destroyRef = inject(DestroyRef) private readonly timers = new Set<ReturnType<typeof setTimeout>>() /** Seed rows; later edits live in the component's own state, as in React's useState initialiser. */ @Input() set initialFlags(value: FeatureFlag[] | undefined) { this.flags.set(value ?? defaultFlags) } @Input('class') className?: string readonly flags = signal<FeatureFlag[]>(defaultFlags) readonly searchQuery = signal('') readonly selectedStatus = signal<FeatureFlagStatusFilter>('all') readonly copiedKey = signal<string | null>(null) readonly filteredFlags = computed(() => { const query = this.searchQuery().trim().toLowerCase() const status = this.selectedStatus() return this.flags().filter((flag) => { if (status === 'enabled' && !flag.enabled) return false if (status === 'disabled' && flag.enabled) return false if (!query) return true return ( flag.name.toLowerCase().includes(query) || flag.key.toLowerCase().includes(query) || flag.description.toLowerCase().includes(query) || flag.tags.some((t) => t.toLowerCase().includes(query)) ) }) }) constructor() { this.destroyRef.onDestroy(() => { for (const t of this.timers) clearTimeout(t) this.timers.clear() }) } get rootClass(): string { return cn('w-full space-y-6', this.className) } toggleFlag(id: string): void { this.flags.update((prev) => prev.map((flag) => (flag.id === id ? { ...flag, enabled: !flag.enabled } : flag))) } deleteFlag(id: string): void { this.flags.update((prev) => prev.filter((flag) => flag.id !== id)) } copyFlagKey(key: string): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(key) this.copiedKey.set(key) const t = setTimeout(() => { this.timers.delete(t) this.copiedKey.update((curr) => (curr === key ? null : curr)) }, 2000) this.timers.add(t) } } statusButtonClass(value: FeatureFlagStatusFilter): string { return cn( 'min-h-6 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', this.selectedStatus() === value ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } rowClass(flag: FeatureFlag): string { return cn('hover:bg-muted/40 transition-colors', !flag.enabled && 'opacity-75 dark:opacity-70') } statusTextClass(flag: FeatureFlag): string { return cn('text-xs font-medium', flag.enabled ? 'text-success dark:text-success' : 'text-muted-foreground') } rolloutLabel(flag: FeatureFlag): string { if (!flag.enabled) return 'Inactive' return flag.rollout === 100 ? 'All users' : 'Ramp' } envBadgeClass(flag: FeatureFlag, key: keyof EnvironmentState): string { return cn( 'inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-xs font-medium', flag.environments[key] ? 'bg-success/10 text-success border-success/20' : 'bg-muted/40 text-muted-foreground border-transparent opacity-60', ) } envDotClass(flag: FeatureFlag, key: keyof EnvironmentState): string { return cn('size-1.5 rounded-full', flag.environments[key] ? 'bg-success' : 'bg-muted-foreground/40') } }src/app/components/blocks/feature-flags-manager/index.ts0.2 kB
export { UiFeatureFlagsManagerComponent, defaultFlags, type EnvironmentState, type FeatureFlag, type FeatureFlagStatusFilter, } from './feature-flags-manager.component'
Raw manifest:https://uipkge.dev/r/angular/feature-flags-manager.json