Menu

Change language

GitHub repo

Integrations — Hub Diagram

blockmarketing

Radial integration diagram: a central platform node connected by drawn SVG spokes to eight orbiting service nodes, with a legend splitting sources from destinations.

Installation

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

Variants

Loading interactive previews…

Schema

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

HubNode
interface HubNode {
  mark: string
  name: string
  direction: string
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/integrations-hub-diagram/integrations-hub-diagram.component.ts6.2 kB
    // Radial integration diagram: a central platform node connected by drawn SVG
    // spokes to eight orbiting service nodes, with a legend splitting sources from
    // destinations. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { ArrowRight, Boxes, LucideAngularModule } 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'
    
    interface HubNode {
      mark: string
      name: string
      direction: string
    }
    
    interface PlacedNode extends HubNode {
      x: number
      y: number
    }
    
    const nodes: HubNode[] = [
      { mark: 'SF', name: 'Snowflake', direction: 'source' },
      { mark: 'BQ', name: 'BigQuery', direction: 'source' },
      { mark: 'PG', name: 'Postgres', direction: 'source' },
      { mark: 'DT', name: 'dbt', direction: 'source' },
      { mark: 'SL', name: 'Slack', direction: 'destination' },
      { mark: 'PD', name: 'PagerDuty', direction: 'destination' },
      { mark: 'LK', name: 'Looker', direction: 'destination' },
      { mark: 'HX', name: 'Hex', direction: 'destination' },
    ]
    
    // Positions are computed on a circle in a 100x100 user-space viewBox, so any
    // number of nodes distributes evenly without touching the markup.
    const RADIUS = 38
    const placed: PlacedNode[] = nodes.map((node, index) => {
      const angle = (index / nodes.length) * Math.PI * 2 - Math.PI / 2
      return { ...node, x: 50 + RADIUS * Math.cos(angle), y: 50 + RADIUS * Math.sin(angle) }
    })
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-integrations-hub-diagram, [ui-integrations-hub-diagram]',
      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],
      template: `
        <section data-slot="integrations-hub-diagram" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
            <div class="grid items-center gap-12 lg:grid-cols-[1fr_1.1fr] lg:gap-16">
              <div>
                <span ui-badge variant="secondary">Architecture</span>
                <h2 class="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
                  One hub between your warehouse and everything downstream
                </h2>
                <p class="text-muted-foreground mt-3 leading-relaxed">
                  Sources stay read-only and in place. Destinations receive certified metrics rather than raw extracts, so
                  nothing downstream can quietly redefine a number.
                </p>
    
                <dl class="mt-8 flex flex-wrap gap-x-8 gap-y-3 text-sm">
                  <div class="flex items-center gap-2">
                    <span class="bg-primary size-2 rounded-full" aria-hidden="true"></span>
                    <dt class="font-medium">Sources</dt>
                    <dd class="text-muted-foreground">read-only</dd>
                  </div>
                  <div class="flex items-center gap-2">
                    <span class="bg-muted-foreground/50 size-2 rounded-full" aria-hidden="true"></span>
                    <dt class="font-medium">Destinations</dt>
                    <dd class="text-muted-foreground">certified metrics only</dd>
                  </div>
                </dl>
    
                <button ui-button class="mt-8">
                  See the reference architecture
                  <lucide-icon [img]="ArrowRight" class="ml-2 size-4" aria-hidden="true" />
                </button>
              </div>
    
              <div ui-card>
                <div ui-card-content class="p-6">
                  <div class="relative aspect-square">
                    <!-- Spokes are drawn in the same 100-unit space the nodes are
                         placed in, so line ends always meet the node centres. -->
                    <svg class="absolute inset-0 size-full" viewBox="0 0 100 100" aria-hidden="true">
                      @for (node of placed; track node.name) {
                        <line
                          x1="50"
                          y1="50"
                          [attr.x2]="node.x"
                          [attr.y2]="node.y"
                          class="stroke-border"
                          stroke-width="0.4"
                          [attr.stroke-dasharray]="node.direction === 'destination' ? '2 2' : null"
                        />
                      }
                      <circle cx="50" cy="50" [attr.r]="radius" class="stroke-border/60" fill="none" stroke-width="0.3" />
                    </svg>
    
                    <div
                      class="border-border bg-card absolute top-1/2 left-1/2 flex size-20 -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center gap-1 rounded-xl border shadow-sm"
                    >
                      <lucide-icon [img]="Boxes" class="text-primary size-5" aria-hidden="true" />
                      <span class="text-xs font-semibold tracking-tight">Metric layer</span>
                    </div>
    
                    @for (node of placed; track node.name) {
                      <div
                        class="absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-1"
                        [style.left]="node.x + '%'"
                        [style.top]="node.y + '%'"
                      >
                        <span [class]="nodeTileClass(node)" aria-hidden="true">
                          {{ node.mark }}
                        </span>
                        <span class="text-muted-foreground text-xs whitespace-nowrap">{{ node.name }}</span>
                      </div>
                    }
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiIntegrationsHubDiagramComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly Boxes = Boxes
    
      protected readonly placed = placed
      protected readonly radius = RADIUS
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      nodeTileClass(node: HubNode): string {
        return cn(
          'border-border bg-background flex size-10 items-center justify-center rounded-lg border font-mono text-xs font-semibold',
          node.direction === 'source' ? 'text-foreground' : 'text-muted-foreground',
        )
      }
    }
    
  • src/app/components/blocks/integrations-hub-diagram/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/integrations-hub-diagram.json