Menu

Change language

GitHub repo

Integrations — Search Directory

blockmarketing

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.

Installation

$npx uipkge-ng@latest add integrations-search-directory
Manifest:https://uipkge.dev/r/angular/integrations-search-directory.jsonInstalls to:src/app/components/blocks/integrations-search-directory/

Variants

Loading interactive previews…

Schema

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

DirectoryIntegration
interface 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

Raw manifest:https://uipkge.dev/r/angular/integrations-search-directory.json