Skip to content

NIFI-15816 - Connector wizard#11126

Merged
mcgilman merged 5 commits intoapache:mainfrom
rfellows:NIFI-15816
Apr 14, 2026
Merged

NIFI-15816 - Connector wizard#11126
mcgilman merged 5 commits intoapache:mainfrom
rfellows:NIFI-15816

Conversation

@rfellows
Copy link
Copy Markdown
Contributor

NIFI-15816: Add Generic Connector Wizard to NiFi frontend (shared library)

Description

This change introduces the generic connector configuration wizard into the NiFi frontend. In NiFi, connectors represent managed data-pipeline components whose configuration is often too rich for a single form. The Connector Wizard is a guided, multi-step experience that walks operators through property groups defined by the connector's descriptors, validates input, supports connectivity verification, and finally applies the configuration to the running system.

The wizard supports two UI modes:

  • Generic (auto-generated) UI -- Steps and fields are driven from server-provided configuration metadata (property groups, types, dependencies, allowable values, etc.). The shared library renders the appropriate controls without connector-specific Angular code.
  • Custom UI -- A connector can supply an external URL; the host application embeds that UI in an iframe. The parent page and the embedded UI coordinate through a typed postMessage protocol so custom UIs can request navigation, report save/verify lifecycle events, and receive cluster-related acknowledgments (for example disconnected-node acknowledgment for mutating NiFi requests) without tight coupling to NiFi's internal state shape.

This PR is intentionally library-first: reusable wizard UI, state, services, and types live under nifi-frontend/.../libs/shared/ so multiple feature areas can host the same wizard with different route shells and store wiring.

Architecture

Layering

  • Presentation -- ConnectorWizardComponent orchestrates the shell (stepper / navigation chrome, summary step, documentation side panel, banners). Step content is composed from generic building blocks (WizardComponent, property-group cards, ConnectorPropertyInput, step actions, verification UX).
  • State -- NgRx SignalStore with a composed withConnectorWizard() feature encapsulates wizard lifecycle: loading the connector, per-step dirty/saved state, verification results, secrets/assets, dynamic allowable values, summary refresh, and apply. An abstract ConnectorWizardStore class is the Angular DI token; shared components inject that token. Hosts provide either StandardConnectorWizardStore (signalStore(withConnectorWizard())) or a custom store that composes withConnectorWizard() with app-specific step rules.
  • API boundary -- ConnectorConfigurationService centralizes REST calls for loading connector entities, saving steps, verification, applying configuration, uploading assets, and related operations used by the store's rxMethod-style effects.
  • Iframe protocol -- ConnectorMessageClient (in the shared library) is the child/iframe API: typed methods emit postMessage to window.parent with origin scoping (derived from document.referrer), and exposes streams such as disconnectedNodeAcknowledgment$() with strict validation of inbound parent messages. ConnectorMessageHost (NiFi app: pages/connectors/service/connector-message-host.service.ts) is the parent listener: it validates origin, optional iframe contentWindow matching (anti-spoofing), and message shape via isConnectorMessage(), then routes events (e.g. router navigation). The canonical message contracts live in connector-message.types.ts (CONNECTOR_MESSAGE_NAMESPACE, discriminated unions, type guards).
  • Forms -- ConnectorPropertyInput implements ControlValueAccessor so reactive forms can render STRING, BOOLEAN, SECRET, ASSET, and related descriptor-driven types consistently, including dynamic allowable values and asset upload hooks.

Component diagram

flowchart TB
    subgraph HostApp["NiFi app (host)"]
        CC["Connector configure route / shell"]
        CMH["ConnectorMessageHost\n(parent postMessage listener)"]
        CC --> CMH
    end

    subgraph SharedLib["libs/shared"]
        CW["ConnectorWizardComponent"]
        CS["ConnectorConfigurationStepComponent"]
        SUM["ConnectorConfigurationSummaryStepComponent"]
        CPI["ConnectorPropertyInput"]
        WZ["WizardComponent"]
        CMC["ConnectorMessageClient\n(iframe child API)"]
        Store["ConnectorWizardStore (DI token)\nStandardConnectorWizardStore\n+ withConnectorWizard()"]
        API["ConnectorConfigurationService"]
        Types["Types & validation utils\n(connector config, messages)"]

        CW --> CS
        CW --> SUM
        CS --> WZ
        CS --> CPI
        CW --> Store
        CS --> Store
        SUM --> Store
        Store --> API
        CMC --> Types
        CMH --> Types
    end

    CC --> CW
    CMC -.->|"postMessage\n(origin-checked)"| CMH
Loading

Iframe support and security

Embedded custom UIs use ConnectorMessageClient to emit structured events (navigation, step saved/errors, verify started/success/error, config applied/errors, UI ready). The parent uses ConnectorMessageHost.startListening() with an explicit expected origin (from the connector's configurationUrl) and optional iframe element resolution so event.source must match the real iframe window when available. Inbound messages are accepted only if they pass namespace + type validation (isConnectorMessage / isParentToConnectorMessage). Child-side disconnectedNodeAcknowledgment$() similarly requires matching origin, event.source === window.parent, and validated payload shape -- a fail-closed posture for cross-window messaging.

Key design decisions

  • NifiSpinnerDirective (and spinner companion) moved into libs/shared so shared wizard code does not duplicate NiFi's native loading affordance.

User flow (configure -> verify -> apply)

sequenceDiagram
    actor User
    participant Host as Host page<br/>(Connector configure)
    participant Wiz as ConnectorWizard<br/>(shared)
    participant Store as ConnectorWizardStore<br/>(withConnectorWizard)
    participant API as ConnectorConfigurationService
    participant IF as Custom UI<br/>(optional iframe)

    User->>Host: Open connector configuration
    Host->>Wiz: Host provides store + renders wizard
    Wiz->>Store: initializeWithConnector / loadConnector
    Store->>API: Fetch connector + step metadata
    API-->>Store: Connector + step configs
    Store-->>Wiz: Visible steps, working configuration

    alt Generic step
        User->>Wiz: Edit properties (CVA inputs)
        Wiz->>Store: updateUnsavedStepValues / markStepDirty
    else Custom step (iframe)
        IF->>Host: postMessage (step events) via ConnectorMessageClient
        Host->>Store: Route events / state updates
    end

    User->>Wiz: Save step / advance
    Store->>API: saveStep (per step)
    API-->>Store: Persisted step configuration

    User->>Wiz: Verify (step or all)
    Store->>API: verify endpoints
    API-->>Store: Verification results / errors
    Wiz-->>User: Inline errors, banners, verification dialog progress

    User->>Wiz: Apply configuration
    Store->>API: applyConfiguration
    API-->>Store: Success or error
    Wiz-->>User: Snackbar + completion / summary refresh
Loading

What's changed (high level)

libs/shared

  • New connector wizard module: shell components, SignalStore + withConnectorWizard, step dependency utilities, wizard context banner, documentation panel (markdown), custom step directive, summary and configuration steps, step actions, ConnectorPropertyInput, shared wizard primitives, ConnectorConfigurationService, ConnectorMessageClient, UploadService, ActiveStepService, value-reference helpers, connector validation utilities, and expanded type surface for connector configuration and messages.
  • NifiSpinnerDirective relocated from the app into shared (re-exports updated).
  • Barrels (components, directives, services, types, index) updated to export the new public API.

apps/nifi

  • Connectors area: iframe host wiring (ConnectorMessageHost, connector-configure integration), postMessage to iframe for disconnected-node acknowledgment after connector-ui-ready, and related component updates.
  • Broad set of import rewires across existing dialogs/forms where spinner directives now come from @nifi/shared instead of a previous local path.

Testing

  • Vitest suites ported and adapted to NiFi's frontend test setup.
  • Coverage is heavily weighted toward the SignalStore (connector-wizard.store.spec.ts), configuration step (large component spec), and summary step (extensive table/validation scenarios), plus utilities, wizard shell, step actions, documentation panel, context banner, custom step directive, and iframe host behavior.

@rfellows rfellows added the ui Pull requests for work relating to the user interface label Apr 10, 2026
@mcgilman
Copy link
Copy Markdown
Contributor

Reviewing...

Copy link
Copy Markdown
Contributor

@mcgilman mcgilman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @rfellows!

I've noted a couple items below. Also, Would it be possible to avoid use of any where we already have a type defined. I think we probably already have many defined, just a matter of using them (specific ones dealing with the property descriptors).

Also, I noted the formatting of the markdown step documentation isn't quite right and the are some font size and layout issues in the property group card in the summary page of the wizard.

Comment on lines +146 to +154
getCustomStepTemplate(stepName: string): TemplateRef<{ $implicit: string }> | null {
const directives = this.customStepDirectives();
if (this.customStepMap.size === 0 && directives.length > 0) {
directives.forEach((dir) => {
this.customStepMap.set(dir.stepName(), dir.templateRef);
});
}
return this.customStepMap.get(stepName) ?? null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCustomStepTemplate builds the map only when empty. If contentChildren/WizardCustomStepDirective changes after first use, the map becomes stale.

private customStepMap = computed(() => {
    const map = new Map<string, TemplateRef<{ $implicit: string }>>();
    for (const dir of this.customStepDirectives()) {
        map.set(dir.stepName(), dir.templateRef);
    }
    return map;
});
getCustomStepTemplate(stepName: string): TemplateRef<{ $implicit: string }> | null {
    return this.customStepMap().get(stepName) ?? null;
}

* If no consumer subscribes, the wizard falls back to the default behavior
* of calling step.select() immediately (backward compatible).
*/
@Output() beforeStepChange = new EventEmitter<BeforeStepChangeEvent>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use signal output here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, the onHeaderClick method uses this.beforeStepChange.observed to conditionally decide whether to emit the event or fall back to step.select(). Angular's signal output() returns OutputEmitterRef which doesn't expose .observed, so this conversion isn't possible without reworking the conditional logic.

Copy link
Copy Markdown
Contributor

@mcgilman mcgilman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates @rfellows!

@mcgilman mcgilman merged commit 1af4ba3 into apache:main Apr 14, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui Pull requests for work relating to the user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants