You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by "rfellows (via GitHub)" <gi...@apache.org> on 2026/04/10 17:56:40 UTC
[PR] NIFI-15816 - Connector wizard [nifi]
rfellows opened a new pull request, #11126:
URL: https://github.com/apache/nifi/pull/11126
# 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
```mermaid
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
```
## 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)
```mermaid
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
```
## 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.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] NIFI-15816 - Connector wizard [nifi]
Posted by "rfellows (via GitHub)" <gi...@apache.org>.
rfellows commented on code in PR #11126:
URL: https://github.com/apache/nifi/pull/11126#discussion_r3075139316
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/wizard/wizard.component.ts:
##########
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, EventEmitter, Output, ViewEncapsulation } from '@angular/core';
+import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
+import { MatStep, MatStepHeader, MatStepper } from '@angular/material/stepper';
+import { StepState } from '@angular/cdk/stepper';
+import { NgTemplateOutlet } from '@angular/common';
+import { CdkStepper } from '@angular/cdk/stepper';
+
+export interface BeforeStepChangeEvent {
+ currentIndex: number;
+ targetIndex: number;
+}
+
+@Component({
+ selector: 'wizard',
+ imports: [NgTemplateOutlet, MatStepHeader],
+ templateUrl: 'wizard.component.html',
+ styleUrl: 'wizard.component.scss',
+ providers: [
+ { provide: CdkStepper, useExisting: Wizard },
+ { provide: STEPPER_GLOBAL_OPTIONS, useValue: { showError: true, displayDefaultIndicatorType: true } }
+ ],
+ encapsulation: ViewEncapsulation.None
+})
+export class Wizard extends MatStepper {
+ /**
+ * Emitted when a step header is clicked, before any navigation occurs.
+ * If a consumer subscribes to this output, the wizard will NOT automatically
+ * call step.select(). The consumer is responsible for calling goToStep()
+ * to complete the navigation.
+ *
+ * If no consumer subscribes, the wizard falls back to the default behavior
+ * of calling step.select() immediately (backward compatible).
+ */
+ @Output() beforeStepChange = new EventEmitter<BeforeStepChangeEvent>();
Review Comment:
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.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] NIFI-15816 - Connector wizard [nifi]
Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #11126:
URL: https://github.com/apache/nifi/pull/11126#discussion_r3074955134
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.ts:
##########
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Component,
+ computed,
+ contentChildren,
+ DestroyRef,
+ input,
+ Injector,
+ OnDestroy,
+ OnInit,
+ output,
+ TemplateRef,
+ viewChild,
+ ChangeDetectorRef,
+ inject
+} from '@angular/core';
+import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
+import { NgTemplateOutlet } from '@angular/common';
+import { StepperSelectionEvent } from '@angular/cdk/stepper';
+import { MatButton } from '@angular/material/button';
+import { MatStep, MatStepLabel, MatStepContent, MatStepperIcon } from '@angular/material/stepper';
+import { filter } from 'rxjs/operators';
+import { Wizard, BeforeStepChangeEvent } from '../wizard/wizard.component';
+import { ActiveStepService } from '../../services/active-step.service';
+import { ConnectorConfigurationSummaryStep } from '../connector-configuration-summary-step/connector-configuration-summary-step.component';
+import { ConfigurationStepConfiguration, ConnectorConfiguration, ConnectorEntity } from '../../types';
+import { ConnectorWizardStore } from './connector-wizard.store';
+import { WizardCustomStepDirective } from './wizard-custom-step.directive';
+import { SharedConnectorConfigurationStep } from './connector-configuration-step/connector-configuration-step.component';
+
+@Component({
+ selector: 'connector-wizard',
+ standalone: true,
+ imports: [
+ NgTemplateOutlet,
+ MatButton,
+ MatStep,
+ MatStepLabel,
+ MatStepContent,
+ MatStepperIcon,
+ Wizard,
+ SharedConnectorConfigurationStep,
+ ConnectorConfigurationSummaryStep
+ ],
+ providers: [ActiveStepService],
+ templateUrl: './connector-wizard.component.html',
+ styleUrl: './connector-wizard.component.scss',
+ host: {
+ class: 'w-full h-full'
+ }
+})
+export class ConnectorWizard implements OnInit, OnDestroy {
+ wizardStore = inject(ConnectorWizardStore);
+ private cdr = inject(ChangeDetectorRef);
+ private destroyRef = inject(DestroyRef);
+ private injector = inject(Injector);
+ private activeStepService = inject(ActiveStepService);
+
+ /**
+ * Tracks the target step index for deferred navigation after a save completes.
+ * When null, the default behavior is to advance forward (wizard.next()).
+ * When set, navigation goes to the specific index (for back button and header clicks).
+ * Reset to null after each navigation is handled.
+ */
+ private pendingTargetIndex: number | null = null;
+
+ readonly connector = input<ConnectorEntity | null>(null);
+ readonly summaryLabel = input('Review & Apply');
+ readonly summaryConfiguration = input<ConnectorConfiguration | null>(null);
+ readonly stepNameMapping = input<{ [displayName: string]: string[] }>({});
+ readonly preWizardContent = input<TemplateRef<unknown> | null>(null);
+
+ readonly summaryVisibleStepNames = computed(() => {
+ const curated = this.summaryConfiguration();
+ if (curated) {
+ return curated.configurationStepConfigurations.map((s) => s.configurationStepName);
+ }
+ return this.wizardStore.visibleStepNames();
+ });
+
+ readonly navigateBack = output<void>();
+
+ readonly wizard = viewChild<Wizard>('wizard');
+ readonly customStepDirectives = contentChildren(WizardCustomStepDirective);
+
+ private customStepMap = new Map<string, TemplateRef<{ $implicit: string }>>();
+
+ ngOnInit(): void {
+ const connector = this.connector();
+ if (connector) {
+ this.wizardStore.initializeWithConnector(connector);
+ this.wizardStore.loadSecrets(connector.id);
+ }
+
+ toObservable(this.wizardStore.pendingStepAdvancement, { injector: this.injector })
+ .pipe(
+ filter((pending) => pending === true),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ this.cdr.detectChanges();
+
+ const wiz = this.wizard();
+ if (wiz) {
+ if (this.pendingTargetIndex !== null) {
+ wiz.goToStep(this.pendingTargetIndex);
+ } else {
+ wiz.next();
+ }
+ }
+
+ this.pendingTargetIndex = null;
+ this.wizardStore.clearPendingStepAdvancement();
+ });
+
+ toObservable(this.wizardStore.bannerErrors, { injector: this.injector })
+ .pipe(
+ filter((errors) => errors.length > 0),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ this.pendingTargetIndex = null;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.wizardStore.resetState();
+ }
+
+ 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;
+ }
Review Comment:
`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;
}
```
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/wizard/wizard.component.ts:
##########
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, EventEmitter, Output, ViewEncapsulation } from '@angular/core';
+import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
+import { MatStep, MatStepHeader, MatStepper } from '@angular/material/stepper';
+import { StepState } from '@angular/cdk/stepper';
+import { NgTemplateOutlet } from '@angular/common';
+import { CdkStepper } from '@angular/cdk/stepper';
+
+export interface BeforeStepChangeEvent {
+ currentIndex: number;
+ targetIndex: number;
+}
+
+@Component({
+ selector: 'wizard',
+ imports: [NgTemplateOutlet, MatStepHeader],
+ templateUrl: 'wizard.component.html',
+ styleUrl: 'wizard.component.scss',
+ providers: [
+ { provide: CdkStepper, useExisting: Wizard },
+ { provide: STEPPER_GLOBAL_OPTIONS, useValue: { showError: true, displayDefaultIndicatorType: true } }
+ ],
+ encapsulation: ViewEncapsulation.None
+})
+export class Wizard extends MatStepper {
+ /**
+ * Emitted when a step header is clicked, before any navigation occurs.
+ * If a consumer subscribes to this output, the wizard will NOT automatically
+ * call step.select(). The consumer is responsible for calling goToStep()
+ * to complete the navigation.
+ *
+ * If no consumer subscribes, the wizard falls back to the default behavior
+ * of calling step.select() immediately (backward compatible).
+ */
+ @Output() beforeStepChange = new EventEmitter<BeforeStepChangeEvent>();
Review Comment:
Could we use signal output here?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] NIFI-15816 - Connector wizard [nifi]
Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on PR #11126:
URL: https://github.com/apache/nifi/pull/11126#issuecomment-4237960990
Reviewing...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] NIFI-15816 - Connector wizard [nifi]
Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman merged PR #11126:
URL: https://github.com/apache/nifi/pull/11126
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org