This is an automated email from the ASF dual-hosted git repository.

zehnder pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new b2c1714f09 feat(#4139): allow manual upload of sample event for 
adapter schema (#4141)
b2c1714f09 is described below

commit b2c1714f09c7d3b5881d1430876a3c5325938fc6
Author: Philipp Zehnder <[email protected]>
AuthorDate: Mon Feb 2 12:35:36 2026 +0100

    feat(#4139): allow manual upload of sample event for adapter schema (#4141)
---
 ui/cypress/support/utils/connect/ConnectBtns.ts    |  18 +++
 ui/cypress/support/utils/connect/ConnectUtils.ts   |  11 ++
 ui/cypress/tests/connect/uploadSampleEvent.spec.ts | 131 +++++++++++++++++++++
 .../adapter-configuration-state.service.ts         |  64 ++++++++++
 .../adapter-configuration.component.ts             |   8 +-
 .../adapter-event-preview.component.html           |   2 +-
 .../adapter-event-preview.component.ts             |  20 +++-
 .../configure-schema.component.html                |   1 +
 .../configure-schema/configure-schema.component.ts |  20 ++++
 .../adapter-result-preview.component.html          |  12 +-
 .../adapter-result-preview.component.ts            |  12 +-
 .../adapter-sample-preview.component.html          |  23 ++--
 .../adapter-sample-preview.component.ts            |  17 +--
 .../adapter-script-editor.component.html           |  24 ++--
 .../adapter-script-editor.component.ts             |  28 ++---
 ui/src/app/connect/connect.module.ts               |   2 +
 .../upload-sample-event-dialog.component.html      |  63 ++++++++++
 .../upload-sample-event-dialog.component.ts        |  61 ++++++++++
 18 files changed, 456 insertions(+), 61 deletions(-)

diff --git a/ui/cypress/support/utils/connect/ConnectBtns.ts 
b/ui/cypress/support/utils/connect/ConnectBtns.ts
index 9caf3610e6..a036502496 100644
--- a/ui/cypress/support/utils/connect/ConnectBtns.ts
+++ b/ui/cypress/support/utils/connect/ConnectBtns.ts
@@ -272,6 +272,24 @@ export class ConnectBtns {
         });
     }
 
+    public static uploadSampleBtn() {
+        return cy.dataCy('connect-upload-sample-button', {
+            timeout: 10000,
+        });
+    }
+
+    public static uploadSampleDialogTextarea() {
+        return cy.dataCy('upload-sample-event-textarea', {
+            timeout: 10000,
+        });
+    }
+
+    public static uploadSampleDialogSubmitBtn() {
+        return cy.dataCy('upload-sample-event-submit', {
+            timeout: 10000,
+        });
+    }
+
     public static configureFieldsEventPreviewResult() {
         return cy.dataCy('configure-fields-event-preview-result', {
             timeout: 10000,
diff --git a/ui/cypress/support/utils/connect/ConnectUtils.ts 
b/ui/cypress/support/utils/connect/ConnectUtils.ts
index 97f0e0635e..4e635d8758 100644
--- a/ui/cypress/support/utils/connect/ConnectUtils.ts
+++ b/ui/cypress/support/utils/connect/ConnectUtils.ts
@@ -409,6 +409,17 @@ export class ConnectUtils {
             .type(script);
     }
 
+    public static uploadSampleEvent(samplePayload: string) {
+        ConnectBtns.uploadSampleBtn().click();
+        ConnectBtns.uploadSampleDialogTextarea()
+            .should('be.visible')
+            .clear()
+            .type(samplePayload, { parseSpecialCharSequences: false });
+        ConnectBtns.uploadSampleDialogSubmitBtn()
+            .should('not.be.disabled')
+            .click();
+    }
+
     public static addScriptAsScriptTemplate(
         templateName: string,
         script: string,
diff --git a/ui/cypress/tests/connect/uploadSampleEvent.spec.ts 
b/ui/cypress/tests/connect/uploadSampleEvent.spec.ts
new file mode 100644
index 0000000000..06ccb5c6cd
--- /dev/null
+++ b/ui/cypress/tests/connect/uploadSampleEvent.spec.ts
@@ -0,0 +1,131 @@
+/*
+ * 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 { AdapterBuilder } from '../../support/builder/AdapterBuilder';
+import { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
+import { ConnectBtns } from '../../support/utils/connect/ConnectBtns';
+import { SharedBtns } from '../../support/utils/shared/SharedBtns';
+import { SharedUtils } from '../../support/utils/shared/SharedUtils';
+
+describe('Upload sample event during schema configuration', () => {
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+
+        const adapterConfiguration =
+            buildMachineDataSimulator('Upload Sample Test');
+        setupAdapter(adapterConfiguration);
+    });
+
+    it('Uses uploaded sample and refreshes fields on warning', () => {
+        ConnectBtns.configureSchemaNextBtn().click();
+        ConnectUtils.eventSchemaWithFieldsShouldBeVisible();
+        ConnectBtns.configureFieldsBackBtn().click();
+
+        uploadSample();
+
+        ConnectBtns.configureSchemaEventPreviewOriginal().should(
+            'contain.text',
+            '"uploadedSample": true',
+        );
+
+        ConnectBtns.configureSchemaNextBtn().click();
+        SharedUtils.confirmDialogVisible();
+        SharedBtns.confirmDialogConfirmBtn().click();
+
+        ConnectUtils.eventSchemaWithFieldsShouldBeVisible();
+        ConnectBtns.configureFieldsEventPreviewResult().should(
+            'contain.text',
+            'uploadedSample',
+        );
+    });
+
+    it('Uses uploaded sample with script enabled after fields roundtrip', () 
=> {
+        ConnectBtns.scriptActiveToggle().click();
+
+        ConnectBtns.configureSchemaNextBtn().click();
+        ConnectUtils.eventSchemaWithFieldsShouldBeVisible();
+        ConnectBtns.configureFieldsBackBtn().click();
+
+        uploadSample();
+
+        ConnectBtns.configureSchemaEventPreviewOriginal().should(
+            'contain.text',
+            'uploadedSample',
+        );
+        ConnectBtns.configureSchemaEventPreviewResult().should(
+            'contain.text',
+            'uploadedSample',
+        );
+
+        ConnectBtns.configureSchemaNextBtn().click();
+        SharedUtils.confirmDialogVisible();
+        SharedBtns.confirmDialogConfirmBtn().click();
+
+        ConnectUtils.eventSchemaWithFieldsShouldBeVisible();
+        ConnectBtns.configureFieldsEventPreviewResult().should(
+            'contain.text',
+            'uploadedSample',
+        );
+    });
+
+    it('Uses uploaded sample with script enabled without fields roundtrip', () 
=> {
+        ConnectBtns.scriptActiveToggle().click();
+
+        uploadSample();
+
+        ConnectBtns.configureSchemaEventPreviewOriginal().should(
+            'contain.text',
+            'uploadedSample',
+        );
+        ConnectBtns.configureSchemaEventPreviewResult().should(
+            'contain.text',
+            'uploadedSample',
+        );
+
+        ConnectBtns.configureSchemaNextBtn().click();
+
+        ConnectUtils.eventSchemaWithFieldsShouldBeVisible();
+        ConnectBtns.configureFieldsEventPreviewResult().should(
+            'contain.text',
+            'uploadedSample',
+        );
+    });
+
+    const buildMachineDataSimulator = (name: string) =>
+        AdapterBuilder.create('Machine_Data_Simulator')
+            .setName(name)
+            .setTimestampProperty('timestamp')
+            .addInput('input', 'wait-time-ms', '1000')
+            .build();
+
+    const setupAdapter = (adapterConfiguration: any) => {
+        ConnectUtils.goToConnect();
+        ConnectUtils.goToNewAdapterPage();
+        ConnectUtils.selectAdapter(adapterConfiguration.adapterType);
+        ConnectUtils.configureAdapter(adapterConfiguration);
+        ConnectBtns.configureSchemaEventPreviewOriginal().should('be.visible');
+    };
+
+    const uploadSample = () => {
+        const uploadedSample = JSON.stringify({
+            uploadedSample: true,
+            temperature: 42,
+        });
+        ConnectUtils.uploadSampleEvent(uploadedSample);
+    };
+});
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
index ecfad38d74..5468092725 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
@@ -280,6 +280,23 @@ export class AdapterConfigurationStateService {
             });
     }
 
+    public uploadSampleEvent(
+        adapter: AdapterDescription,
+        samplePayload: string,
+    ): void {
+        const trimmed = (samplePayload || '').trim();
+        if (!trimmed) {
+            return;
+        }
+
+        this.sampleRequestSubscription?.unsubscribe();
+        this.sampleRequestSubscription = undefined;
+
+        const parsed = JSON.parse(trimmed);
+        const sample = this.normalizeUploadedSample(parsed);
+        this.applySamplePayload(adapter, sample);
+    }
+
     public runScript(adapter: AdapterDescription): void {
         // 1. Prepare state for loading
         this.updateState({
@@ -412,4 +429,51 @@ export class AdapterConfigurationStateService {
     public reset(): void {
         this._state.set({ ...this.initialState });
     }
+
+    private applySamplePayload(
+        adapter: AdapterDescription,
+        payload: any,
+    ): void {
+        const updatedAdapter = {
+            ...adapter,
+            transformationConfig: { ...adapter.transformationConfig },
+        };
+        updatedAdapter.transformationConfig.inputs = [payload];
+
+        const scriptActive = updatedAdapter.transformationConfig.scriptActive;
+        if (!scriptActive) {
+            updatedAdapter.transformationConfig.outputs =
+                updatedAdapter.transformationConfig.inputs;
+        }
+
+        const transformationConfigurationChanged =
+            this.checkIfTransformationConfigurationChanged(updatedAdapter);
+
+        this.updateState({
+            adapterDescription: updatedAdapter,
+            isGettingSample: false,
+            adapterSettingsChanged: false,
+            adapterSettingsString: JSON.stringify(updatedAdapter.config),
+            transformationConfigurationChanged,
+            sampleFieldStatusInfos: null,
+            sampleError: null,
+        });
+
+        if (scriptActive) {
+            this.runScript(updatedAdapter);
+        }
+    }
+
+    private normalizeUploadedSample(sample: string): string {
+        if (Array.isArray(sample)) {
+            if (sample.length === 0) {
+                throw new Error('Sample array is empty');
+            }
+            return sample[0];
+        }
+        if (sample === null || typeof sample !== 'object') {
+            throw new Error('Sample must be a JSON object');
+        }
+        return sample;
+    }
 }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
index 075f488a69..268ef7303a 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration.component.ts
@@ -101,10 +101,14 @@ export class AdapterConfigurationComponent implements 
OnInit, OnDestroy {
     }
 
     nextConfigureSchema() {
+        const adapter =
+            this.stateService.state().adapterDescription ??
+            this.adapterDescription;
+
         if (this.stateService.state().autoLoadSchema) {
-            this.stateService.getEventSchema(this.adapterDescription);
+            this.stateService.getEventSchema(adapter);
         } else {
-            this.stateService.updateEventPreview(this.adapterDescription);
+            this.stateService.updateEventPreview(adapter);
         }
 
         if (this.stateService.state().transformationConfigurationChanged) {
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.html
 
b/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.html
index 8810b339a9..18570f8d16 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.html
@@ -18,7 +18,7 @@
 
 <section class="jv">
     <div class="jv__content" [class.is-empty]="!hasValue()">
-        @if (hasValue) {
+        @if (hasValue()) {
             <ng-container [ngSwitch]="mode">
                 <pre
                     *ngSwitchCase="'raw'"
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.ts
index 69abfa3b84..b478b1cac4 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-event-preview/adapter-event-preview.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component, computed, Input } from '@angular/core';
+import { Component, computed, Input, signal } from '@angular/core';
 
 export type Mode = 'tree' | 'raw';
 
@@ -27,7 +27,15 @@ export type Mode = 'tree' | 'raw';
     styleUrl: './adapter-event-preview.component.scss',
 })
 export class AdapterEventPreviewComponent {
-    @Input() value: unknown = null;
+    private valueSignal = signal<unknown>(null);
+
+    @Input()
+    set value(val: unknown) {
+        this.valueSignal.set(val);
+    }
+    get value(): unknown {
+        return this.valueSignal();
+    }
 
     /** Optional header title. */
     @Input() title = '';
@@ -46,14 +54,16 @@ export class AdapterEventPreviewComponent {
 
     @Input() dataCy = '';
 
-    hasValue = computed(() => this.value !== null && this.value !== undefined);
+    hasValue = computed(
+        () => this.valueSignal() !== null && this.valueSignal() !== undefined,
+    );
 
     prettyJson = computed(() => {
         try {
-            return JSON.stringify(this.value, null, 2);
+            return JSON.stringify(this.valueSignal(), null, 2);
         } catch {
             // Circular refs or non-serializable input
-            return String(this.value);
+            return String(this.valueSignal());
         }
     });
 }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
index 2aa96b51ba..e317fb3ec3 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
@@ -44,6 +44,7 @@
                 [sourceViewMode]="sourceViewMode()"
                 (sourceViewModeChange)="setSourceViewMode($event)"
                 (getSample)="getSampleEvent()"
+                (uploadSample)="openUploadSampleDialog()"
             ></sp-adapter-sample-preview>
         </div>
 
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
index 7f29649338..c9940cb760 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
@@ -44,6 +44,7 @@ import { TranslateService } from '@ngx-translate/core';
 import { SelectAdapterTransformationTemplateDialogComponent } from 
'../../../dialog/select-adapter-transformation-template-dialog/select-adapter-transformation-template-dialog.component';
 import { Mode } from 
'../adapter-event-preview/adapter-event-preview.component';
 import { MatDialog } from '@angular/material/dialog';
+import { UploadSampleEventDialogComponent } from 
'../../../dialog/upload-sample-event-dialog/upload-sample-event-dialog.component';
 
 @Component({
     selector: 'sp-configure-schema',
@@ -188,6 +189,25 @@ export class ConfigureSchemaComponent implements OnInit {
         this.stateService.getSampleEvent(this.adapterDescription);
     }
 
+    openUploadSampleDialog(): void {
+        const dialogRef = this.dialogService.open(
+            UploadSampleEventDialogComponent,
+            {
+                panelType: PanelType.STANDARD_PANEL,
+                title: this.translateService.instant('Upload sample event'),
+                width: '50vw',
+            },
+        );
+        dialogRef.afterClosed().subscribe(samplePayload => {
+            if (samplePayload) {
+                const adapter =
+                    this.stateService.state().adapterDescription ??
+                    this.adapterDescription;
+                this.stateService.uploadSampleEvent(adapter, samplePayload);
+            }
+        });
+    }
+
     runScript(): void {
         this.stateService.runScript(this.adapterDescription);
     }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.html
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.html
index 3058f8ad06..dc907b38a8 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.html
@@ -24,7 +24,7 @@
 >
     <div header fxFlex="100" fxLayoutAlign="end center">
         <mat-button-toggle-group
-            [value]="resultViewMode"
+            [value]="resultViewMode()"
             [hideSingleSelectionIndicator]="true"
             (valueChange)="resultViewModeChange.emit($event)"
         >
@@ -36,7 +36,7 @@
             </mat-button-toggle>
         </mat-button-toggle-group>
     </div>
-    @if (isRunningScript) {
+    @if (isRunningScript()) {
         <div class="m-lg">
             <div fxFlex="100" fxLayoutAlign="center center" fxLayout="column">
                 <mat-spinner [diameter]="25"></mat-spinner>
@@ -44,17 +44,17 @@
             </div>
         </div>
     } @else {
-        @if (scriptError) {
+        @if (scriptError()) {
             <sp-exception-message
                 class="p-xs"
-                [message]="scriptError"
+                [message]="scriptError()"
                 [showDetails]="true"
             ></sp-exception-message>
         } @else {
             <sp-adapter-event-preview
                 dataCy="configure-schema-event-preview-result"
-                [mode]="resultViewMode"
-                [value]="output"
+                [mode]="resultViewMode()"
+                [value]="output()"
             ></sp-adapter-event-preview>
         }
     }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.ts
index 4ae85a4bae..0b953cadaa 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Component, input, output } from '@angular/core';
 import { Mode } from 
'../../adapter-event-preview/adapter-event-preview.component';
 
 @Component({
@@ -25,10 +25,10 @@ import { Mode } from 
'../../adapter-event-preview/adapter-event-preview.componen
     templateUrl: './adapter-result-preview.component.html',
 })
 export class AdapterResultPreviewComponent {
-    @Input() isRunningScript = false;
-    @Input() scriptError: any;
-    @Input() output: any;
-    @Input() resultViewMode: Mode = 'raw';
+    isRunningScript = input(false);
+    scriptError = input<any>();
+    output = input<any>();
+    resultViewMode = input<Mode>('raw');
 
-    @Output() resultViewModeChange = new EventEmitter<Mode>();
+    resultViewModeChange = output<Mode>();
 }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.html
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.html
index ca0aafaca1..d84955832c 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.html
@@ -25,12 +25,21 @@
             data-cy="connect-get-new-sample-button"
             mat-button
             (click)="getSample.emit()"
+            [disabled]="isSampleLoading()"
         >
             <mat-icon>refresh</mat-icon>
             <span>{{ 'Get new sample' | translate }}</span>
         </button>
+        <button
+            data-cy="connect-upload-sample-button"
+            mat-button
+            (click)="uploadSample.emit()"
+        >
+            <mat-icon>file_upload</mat-icon>
+            <span>{{ 'Upload sample' | translate }}</span>
+        </button>
         <mat-button-toggle-group
-            [value]="sourceViewMode"
+            [value]="sourceViewMode()"
             [hideSingleSelectionIndicator]="true"
             (valueChange)="sourceViewModeChange.emit($event)"
         >
@@ -43,7 +52,7 @@
         </mat-button-toggle-group>
     </div>
 
-    @if (isSampleLoading) {
+    @if (isSampleLoading()) {
         <div
             fxFlex="100"
             fxLayoutAlign="center center"
@@ -54,19 +63,19 @@
             <h5>{{ 'Loading' | translate }}</h5>
         </div>
     } @else {
-        @if (sampleErrorMessage) {
+        @if (sampleErrorMessage()) {
             <sp-exception-message
-                [message]="sampleErrorMessage"
+                [message]="sampleErrorMessage()"
                 [showDetails]="true"
             ></sp-exception-message>
         } @else {
             <sp-show-field-status-infos
-                [fieldStatusInfos]="fieldStatusInfos"
+                [fieldStatusInfos]="fieldStatusInfos()"
             ></sp-show-field-status-infos>
             <sp-adapter-event-preview
                 dataCy="configure-schema-event-preview-original"
-                [mode]="sourceViewMode"
-                [value]="input"
+                [mode]="sourceViewMode()"
+                [value]="input()"
             ></sp-adapter-event-preview>
         }
     }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.ts
index 7853d58a6f..a241e94144 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Component, input, output } from '@angular/core';
 import { Mode } from 
'../../adapter-event-preview/adapter-event-preview.component';
 
 @Component({
@@ -25,12 +25,13 @@ import { Mode } from 
'../../adapter-event-preview/adapter-event-preview.componen
     templateUrl: './adapter-sample-preview.component.html',
 })
 export class AdapterSamplePreviewComponent {
-    @Input() isSampleLoading = false;
-    @Input() sampleErrorMessage: any;
-    @Input() fieldStatusInfos: any;
-    @Input() input: any;
-    @Input() sourceViewMode: Mode = 'raw';
+    isSampleLoading = input(false);
+    sampleErrorMessage = input<any>();
+    fieldStatusInfos = input<any>();
+    input = input<any>();
+    sourceViewMode = input<Mode>('raw');
 
-    @Output() sourceViewModeChange = new EventEmitter<Mode>();
-    @Output() getSample = new EventEmitter<void>();
+    sourceViewModeChange = output<Mode>();
+    getSample = output<void>();
+    uploadSample = output<void>();
 }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.html
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.html
index 8bf4189031..ff06d7c2fa 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.html
@@ -19,29 +19,29 @@
     [panelTitle]="'Transformation' | translate"
     outerMargin="20px 0px"
 >
-    @if (loadingAvailableScriptsError) {
+    @if (loadingAvailableScriptsError()) {
         <sp-alert-banner
             type="error"
             [title]="'Error loading available script languages'"
-            [description]="loadingAvailableScriptsError.cause"
+            [description]="loadingAvailableScriptsError().cause"
         >
         </sp-alert-banner>
     }
 
     <div header fxLayoutAlign="end center" fxFlex="100" fxLayoutGap="5px">
-        @if (scriptActive) {
-            @if (selectedScriptMetadata) {
+        @if (scriptActive()) {
+            @if (selectedScriptMetadata()) {
                 <button
                     mat-button
                     [matMenuTriggerFor]="langMenu"
                     aria-label="Select template language"
                 >
-                    {{ selectedScriptMetadata.name | titlecase }}
+                    {{ selectedScriptMetadata().name | titlecase }}
                     <mat-icon>arrow_drop_down</mat-icon>
                 </button>
 
                 <mat-menu #langMenu="matMenu">
-                    @for (script of availableScripts; track script.language) {
+                    @for (script of availableScripts(); track script.language) 
{
                         <button
                             mat-menu-item
                             (click)="languageChange.emit(script)"
@@ -49,7 +49,7 @@
                             <span>{{ script.name | titlecase }}</span>
                             @if (
                                 script.language ===
-                                selectedScriptMetadata.language
+                                selectedScriptMetadata().language
                             ) {
                                 <mat-icon class="ms-auto">check</mat-icon>
                             }
@@ -77,10 +77,10 @@
 
         <mat-slide-toggle
             data-cy="toggle-script-active"
-            [ngModel]="scriptActive"
+            [ngModel]="scriptActive()"
             (ngModelChange)="toggleScriptActive.emit()"
         >
-            @if (scriptActive) {
+            @if (scriptActive()) {
                 {{ 'Disable script' | translate }}
             } @else {
                 {{ 'Enable script' | translate }}
@@ -88,13 +88,13 @@
         </mat-slide-toggle>
     </div>
 
-    @if (scriptActive) {
+    @if (scriptActive()) {
         <div class="code-editor-outer">
             <ngx-codemirror
                 class="code-editor"
-                [ngModel]="script"
+                [ngModel]="script()"
                 (ngModelChange)="codeChange.emit($event)"
-                [options]="editorOptions"
+                [options]="editorOptions()"
                 data-cy="configure-schema-script-editor"
             ></ngx-codemirror>
         </div>
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.ts
index bafe5f5655..a4364e9efe 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Component, input, output } from '@angular/core';
 import { ScriptMetadata } from '@streampipes/platform-services';
 
 @Component({
@@ -25,18 +25,18 @@ import { ScriptMetadata } from 
'@streampipes/platform-services';
     templateUrl: './adapter-script-editor.component.html',
 })
 export class AdapterScriptEditorComponent {
-    @Input() scriptActive = false;
-    @Input() selectedScriptMetadata: ScriptMetadata;
-    @Input() availableScripts: ScriptMetadata[] = [];
-    @Input() loadingAvailableScriptsError: any;
-    @Input() script = '';
-    @Input() editorOptions: any;
+    scriptActive = input(false);
+    selectedScriptMetadata = input<ScriptMetadata>();
+    availableScripts = input<ScriptMetadata[]>([]);
+    loadingAvailableScriptsError = input<any>();
+    script = input('');
+    editorOptions = input<any>();
 
-    @Output() codeChange = new EventEmitter<string>();
-    @Output() languageChange = new EventEmitter<ScriptMetadata>();
-    @Output() selectTemplate = new EventEmitter<void>();
-    @Output() resetScript = new EventEmitter<void>();
-    @Output() toggleScriptActive = new EventEmitter<void>();
-    @Output() runScript = new EventEmitter<void>();
-    @Output() createTemplate = new EventEmitter<void>();
+    codeChange = output<string>();
+    languageChange = output<ScriptMetadata>();
+    selectTemplate = output<void>();
+    resetScript = output<void>();
+    toggleScriptActive = output<void>();
+    runScript = output<void>();
+    createTemplate = output<void>();
 }
diff --git a/ui/src/app/connect/connect.module.ts 
b/ui/src/app/connect/connect.module.ts
index 8600aa21df..689eb11bb5 100644
--- a/ui/src/app/connect/connect.module.ts
+++ b/ui/src/app/connect/connect.module.ts
@@ -113,6 +113,7 @@ import { ShowFieldStatusInfosComponent } from 
'./components/adapter-configuratio
 import { AdapterScriptEditorComponent } from 
'./components/adapter-configuration/configure-schema/script-editor/adapter-script-editor.component';
 import { AdapterSamplePreviewComponent } from 
'./components/adapter-configuration/configure-schema/sample-preview/adapter-sample-preview.component';
 import { AdapterResultPreviewComponent } from 
'./components/adapter-configuration/configure-schema/result-preview/adapter-result-preview.component';
+import { UploadSampleEventDialogComponent } from 
'./dialog/upload-sample-event-dialog/upload-sample-event-dialog.component';
 
 @NgModule({
     imports: [
@@ -262,6 +263,7 @@ import { AdapterResultPreviewComponent } from 
'./components/adapter-configuratio
         AdapterScriptEditorComponent,
         AdapterSamplePreviewComponent,
         AdapterResultPreviewComponent,
+        UploadSampleEventDialogComponent,
     ],
     providers: [TimestampPipe],
     schemas: [CUSTOM_ELEMENTS_SCHEMA],
diff --git 
a/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.html
 
b/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.html
new file mode 100644
index 0000000000..21a2a981e5
--- /dev/null
+++ 
b/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.html
@@ -0,0 +1,63 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<div class="sp-dialog-container">
+    <div class="sp-dialog-content p-15">
+        <sp-form-field [label]="'Sample event (JSON)' | translate">
+            <mat-form-field class="w-100">
+                <textarea
+                    matInput
+                    [ngModel]="samplePayload()"
+                    (ngModelChange)="samplePayload.set($event)"
+                    class="code-input"
+                    rows="12"
+                    data-cy="upload-sample-event-textarea"
+                ></textarea>
+                @if (isSampleInvalid()) {
+                    <sp-alert-banner
+                        type="error"
+                        [title]="'Invalid JSON' | translate"
+                        [description]="'Sample must be valid JSON.' | 
translate"
+                    >
+                    </sp-alert-banner>
+                }
+            </mat-form-field>
+        </sp-form-field>
+    </div>
+    <mat-divider></mat-divider>
+    <div class="sp-dialog-actions" fxLayoutGap="10px">
+        <button
+            mat-flat-button
+            color="accent"
+            data-cy="upload-sample-event-submit"
+            (click)="submit()"
+            [disabled]="!isSampleValid()"
+        >
+            <mat-icon>check</mat-icon>
+            <span>{{ 'Use sample' | translate }}</span>
+        </button>
+        <button
+            class="mat-basic"
+            mat-flat-button
+            data-cy="upload-sample-event-cancel"
+            (click)="close()"
+        >
+            {{ 'Cancel' | translate }}
+        </button>
+    </div>
+</div>
diff --git 
a/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.ts
 
b/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.ts
new file mode 100644
index 0000000000..5ee1c41df0
--- /dev/null
+++ 
b/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.ts
@@ -0,0 +1,61 @@
+/*
+ * 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, inject, signal } from '@angular/core';
+import { DialogRef } from '@streampipes/shared-ui';
+
+@Component({
+    selector: 'sp-upload-sample-event-dialog',
+    templateUrl: './upload-sample-event-dialog.component.html',
+    standalone: false,
+})
+export class UploadSampleEventDialogComponent {
+    private dialogRef = inject(DialogRef<UploadSampleEventDialogComponent>);
+
+    samplePayload = signal('');
+
+    isSampleValid(): boolean {
+        const trimmed = this.samplePayload().trim();
+        if (!trimmed) {
+            return false;
+        }
+        try {
+            JSON.parse(trimmed);
+            return true;
+        } catch {
+            return false;
+        }
+    }
+
+    isSampleInvalid(): boolean {
+        const trimmed = this.samplePayload().trim();
+        return trimmed.length > 0 && !this.isSampleValid();
+    }
+
+    submit(): void {
+        const trimmed = this.samplePayload().trim();
+        if (!trimmed || !this.isSampleValid()) {
+            return;
+        }
+        this.dialogRef.close(trimmed);
+    }
+
+    close(): void {
+        this.dialogRef.close();
+    }
+}


Reply via email to