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

zehnder pushed a commit to branch 
4139-allow-manual-upload-of-sample-event-for-adapter-schema-inference
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/4139-allow-manual-upload-of-sample-event-for-adapter-schema-inference
 by this push:
     new 051505b1df fix(#4139): Add a dialog to upload sample events in connect
051505b1df is described below

commit 051505b1df8c84f91971bac509015c4360a67f9c
Author: Philipp Zehnder <[email protected]>
AuthorDate: Fri Jan 30 17:02:54 2026 +0100

    fix(#4139): Add a dialog to upload sample events in connect
---
 .../adapter-configuration-state.service.ts         | 65 ++++++++++++++++++++++
 .../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-sample-preview.component.html          |  9 +++
 .../adapter-sample-preview.component.ts            |  1 +
 ui/src/app/connect/connect.module.ts               |  2 +
 .../upload-sample-event-dialog.component.html      | 62 +++++++++++++++++++++
 .../upload-sample-event-dialog.component.ts        | 61 ++++++++++++++++++++
 10 files changed, 237 insertions(+), 6 deletions(-)

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..b5fbc538f8 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,52 @@ 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:
+                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-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/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..d693186ddb 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,10 +25,19 @@
             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"
             [hideSingleSelectionIndicator]="true"
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..78714f798b 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
@@ -33,4 +33,5 @@ export class AdapterSamplePreviewComponent {
 
     @Output() sourceViewModeChange = new EventEmitter<Mode>();
     @Output() getSample = new EventEmitter<void>();
+    @Output() uploadSample = new EventEmitter<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..152420c11a
--- /dev/null
+++ 
b/ui/src/app/connect/dialog/upload-sample-event-dialog/upload-sample-event-dialog.component.html
@@ -0,0 +1,62 @@
+<!--
+  ~ 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"
+                ></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