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