Integrations — Search Directory
blockmarketingSearchable integration directory: a live filter field above category-grouped rows carrying a monogram, blurb, and auth type, with an empty state when nothing matches.
Installation
$pnpm dlx uipkge-ng@latest add integrations-search-directory$npx uipkge-ng@latest add integrations-search-directory$yarn dlx uipkge-ng@latest add integrations-search-directory$bunx uipkge-ng@latest add integrations-search-directoryManifest:
https://uipkge.dev/r/angular/integrations-search-directory.jsonInstalls to:src/app/components/blocks/integrations-search-directory/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.
DirectoryIntegrationinterface DirectoryIntegration {
mark: string
name: string
category: string
blurb: string
auth: string
}Files installed (2)
src/app/components/blocks/integrations-search-directory/integrations-search-directory.component.ts7.5 kB
// Searchable integration directory: a live filter field above category-grouped // rows carrying a monogram, blurb, and auth type, with an empty state when // nothing matches. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowUpRight, LucideAngularModule, Search } 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 { UiSeparatorComponent } from '@/ui/separator/separator.component' interface DirectoryIntegration { mark: string name: string category: string blurb: string auth: string } const integrations: DirectoryIntegration[] = [ { mark: 'SF', name: 'Snowflake', category: 'Warehouses', blurb: 'Query live tables with a read-only role.', auth: 'Key pair', }, { mark: 'BQ', name: 'BigQuery', category: 'Warehouses', blurb: 'Service-account access, per-dataset scope.', auth: 'Service account', }, { mark: 'RS', name: 'Redshift', category: 'Warehouses', blurb: 'Serverless and provisioned clusters.', auth: 'IAM role', }, { mark: 'OK', name: 'Okta', category: 'Identity', blurb: 'Group membership drives row-level scope.', auth: 'SAML + SCIM', }, { mark: 'EI', name: 'Entra ID', category: 'Identity', blurb: 'Directory groups sync on a schedule.', auth: 'SAML + SCIM', }, { mark: 'SL', name: 'Slack', category: 'Alerting', blurb: 'Route alerts to the metric owner’s channel.', auth: 'OAuth', }, { mark: 'PD', name: 'PagerDuty', category: 'Alerting', blurb: 'Page on freshness breaches, not on noise.', auth: 'API key', }, { mark: 'DT', name: 'dbt', category: 'Modelling', blurb: 'Import models as certified metric sources.', auth: 'API token', }, { mark: 'AF', name: 'Airflow', category: 'Modelling', blurb: 'Trigger materialisation from your DAGs.', auth: 'API token', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-integrations-search-directory, [ui-integrations-search-directory]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiInputComponent, UiSeparatorComponent, ], template: ` <section data-slot="integrations-search-directory" [class]="rootClass"> <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28"> <div class="flex flex-wrap items-end justify-between gap-6"> <div> <span ui-badge variant="secondary">Directory</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Find your stack</h2> </div> <div class="relative w-full sm:w-72"> <ui-input [value]="query()" (valueChange)="query.set($event)" class="pl-9" placeholder="Search integrations…" aria-label="Search integrations" [prefixIcon]="searchIcon" /> <ng-template #searchIcon> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> </ng-template> </div> </div> <div ui-card class="mt-8"> <div ui-card-content class="p-2"> @if (groups().length === 0) { <div class="px-4 py-16 text-center"> <p class="text-sm font-medium">No integration matches “{{ query() }}”.</p> <p class="text-muted-foreground mt-1 text-xs"> We ship new connectors most releases — tell us which one you need. </p> <button ui-button variant="outline" size="sm" class="mt-4">Request an integration</button> </div> } @for (group of groups(); track group.category; let last = $last) { <div> <p class="text-muted-foreground px-4 pt-4 pb-2 font-mono text-xs tracking-[0.14em] uppercase"> {{ group.category }} </p> <ul> @for (integration of group.items; track integration.name) { <li> <a href="#" (click)="$event.preventDefault()" class="hover:bg-muted focus-visible:ring-ring group flex items-center gap-4 rounded-lg px-4 py-3 transition-colors focus-visible:ring-2 focus-visible:outline-none" > <span class="border-border text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-md border font-mono text-xs font-semibold" aria-hidden="true" > {{ integration.mark }} </span> <span class="min-w-0 flex-1"> <span class="block text-sm font-medium">{{ integration.name }}</span> <span class="text-muted-foreground block truncate text-xs">{{ integration.blurb }}</span> </span> <span ui-badge variant="outline" class="hidden shrink-0 sm:inline-flex"> {{ integration.auth }} </span> <lucide-icon [img]="ArrowUpRight" class="text-muted-foreground size-4 shrink-0 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" aria-hidden="true" /> </a> </li> } </ul> @if (!last) { <ui-separator class="my-2" /> } </div> } </div> </div> </div> </section> `, }) export class UiIntegrationsSearchDirectoryComponent { protected readonly ArrowUpRight = ArrowUpRight protected readonly Search = Search @Input() set initialQuery(v: string) { this.query.set(v ?? '') } @Input('class') className?: string readonly query = signal('') readonly groups = computed(() => { const q = this.query().trim().toLowerCase() const matches = q ? integrations.filter((i) => `${i.name} ${i.category} ${i.blurb} ${i.auth}`.toLowerCase().includes(q)) : integrations const ordered: { category: string; items: DirectoryIntegration[] }[] = [] const byCategory = new Map<string, DirectoryIntegration[]>() for (const integration of matches) { const list = byCategory.get(integration.category) if (list) list.push(integration) else { const next: DirectoryIntegration[] = [integration] byCategory.set(integration.category, next) ordered.push({ category: integration.category, items: next }) } } return ordered }) get rootClass(): string { return cn('bg-background', this.className) } }src/app/components/blocks/integrations-search-directory/index.ts0.1 kB
export { UiIntegrationsSearchDirectoryComponent } from './integrations-search-directory.component'
Raw manifest:https://uipkge.dev/r/angular/integrations-search-directory.json