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();
+ }
+}