This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch add-multi-language-support-templates in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 23765e02b3a0a127258caf88a753605dc313af48 Author: Dominik Riemer <[email protected]> AuthorDate: Fri Jan 2 13:56:27 2026 +0100 Add multi-language support to connect transformation --- streampipes-connect-transformer-api/pom.xml | 10 ++- .../transformer/api/TransformationEngine.java | 3 +- .../transformer/api/TransformationEngines.java | 13 +++- .../transformer/groovy/GroovyScriptEngine.java | 11 +++- .../transformer/js/GraalJsScriptEngine.java | 16 ++++- .../streampipes/model/connect/ScriptMetadata.java | 12 ++-- .../svcdiscovery/SpServiceRegistration.java | 23 ++++++- .../ExtensionsServiceEndpointGenerator.java | 10 +-- .../endpoint/ExtensionsServiceEndpointUtils.java | 11 ++++ .../TransformationScriptLanguageResource.java | 66 +++++++++++++++++++ .../svcdiscovery/api/ISpServiceDiscovery.java | 4 ++ .../svcdiscovery/SpServiceDiscoveryCore.java | 12 +++- .../StreamPipesExtensionsServiceBase.java | 9 ++- .../lib/apis/connect-script-languages.service.ts | 29 +++++++++ .../lib/apis/connect-script-templates.service.ts | 0 .../src/lib/model/gen/streampipes-model.ts | 47 +++++++++++++- .../platform-services/src/public-api.ts | 1 + .../adapter-configuration-state.service.ts | 14 +++- .../configure-fields-preview.component.scss | 4 +- .../configure-fields-error-message.component.html | 9 ++- .../configure-fields-error-message.component.scss | 2 +- .../configure-schema.component.html | 75 ++++++++++++++++++---- .../configure-schema.component.scss | 4 +- .../configure-schema/configure-schema.component.ts | 71 ++++++++++++++++---- 24 files changed, 390 insertions(+), 66 deletions(-) diff --git a/streampipes-connect-transformer-api/pom.xml b/streampipes-connect-transformer-api/pom.xml index 1278b99369..801c646bb3 100644 --- a/streampipes-connect-transformer-api/pom.xml +++ b/streampipes-connect-transformer-api/pom.xml @@ -31,9 +31,15 @@ <artifactId>streampipes-connect-transformer-api</artifactId> <properties> - <maven.compiler.source>21</maven.compiler.source> - <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> + <dependencies> + <dependency> + <groupId>org.apache.streampipes</groupId> + <artifactId>streampipes-model</artifactId> + <version>0.99.0-SNAPSHOT</version> + </dependency> + </dependencies> + </project> diff --git a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java index cb5b210c1f..11e34c774b 100644 --- a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java +++ b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java @@ -19,6 +19,7 @@ package org.apache.streampipes.connect.transformer.api; import org.apache.streampipes.connect.transformer.api.exception.ScriptCompilationException; +import org.apache.streampipes.model.connect.ScriptMetadata; /** * Compiles code templates into reusable transformers. @@ -28,7 +29,7 @@ public interface TransformationEngine { /** * Identifier of the underlying scripting language. */ - String language(); + ScriptMetadata metadata(); /** * Compile the user-provided script into an executable transformer. diff --git a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java index 300668bae7..99b2a8863c 100644 --- a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java +++ b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java @@ -18,7 +18,10 @@ package org.apache.streampipes.connect.transformer.api; +import org.apache.streampipes.model.connect.ScriptMetadata; + import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -29,10 +32,18 @@ public enum TransformationEngines { private final Map<String, Supplier<TransformationEngine>> transformationEngines = new HashMap<>(); public void registerEngine(Supplier<TransformationEngine> engineSupplier) { - transformationEngines.put(engineSupplier.get().language(), engineSupplier); + transformationEngines.put(engineSupplier.get().metadata().language(), engineSupplier); } public TransformationEngine getTransformationEngine(String language) { return transformationEngines.get(language).get(); } + + public List<ScriptMetadata> getAvailableEngineMetadata() { + return transformationEngines + .values() + .stream() + .map(transformationEngineSupplier -> transformationEngineSupplier.get().metadata()) + .toList(); + } } diff --git a/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java b/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java index 406a33bad6..a217b8b3aa 100644 --- a/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java +++ b/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java @@ -23,6 +23,7 @@ import org.apache.streampipes.connect.transformer.api.TransformationEngine; import org.apache.streampipes.connect.transformer.api.exception.ScriptCompilationException; import org.apache.streampipes.connect.transformer.api.exception.ScriptExecutionException; import org.apache.streampipes.connect.transformer.api.utils.TransformationEngineConversionUtils; +import org.apache.streampipes.model.connect.ScriptMetadata; import groovy.lang.Binding; import groovy.lang.GroovyShell; @@ -35,8 +36,12 @@ import java.util.Map; public class GroovyScriptEngine implements TransformationEngine { @Override - public String language() { - return "groovy"; + public ScriptMetadata metadata() { + return new ScriptMetadata( + "groovy", + "Groovy", + "" + ); } @Override @@ -61,7 +66,7 @@ public class GroovyScriptEngine implements TransformationEngine { binding.setVariable("input", input); Script scriptInstance = InvokerHelper.createScript(scriptClass, binding); Object result = scriptInstance.run(); - return TransformationEngineConversionUtils.ensureMap(result, language()); + return TransformationEngineConversionUtils.ensureMap(result, metadata().language()); } catch (Exception e) { throw new ScriptExecutionException("Groovy template execution failed", e); } diff --git a/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java b/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java index a677d91782..6cb9b267a7 100644 --- a/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java +++ b/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java @@ -22,6 +22,7 @@ import org.apache.streampipes.connect.transformer.api.ScriptTransformer; import org.apache.streampipes.connect.transformer.api.TransformationEngine; import org.apache.streampipes.connect.transformer.api.exception.ScriptCompilationException; import org.apache.streampipes.connect.transformer.api.exception.ScriptExecutionException; +import org.apache.streampipes.model.connect.ScriptMetadata; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.HostAccess; @@ -34,8 +35,17 @@ import java.util.Optional; public class GraalJsScriptEngine implements TransformationEngine { @Override - public String language() { - return "javascript"; + public ScriptMetadata metadata() { + return new ScriptMetadata( + "javascript", + "JavaScript", + """ + // returns the same event + function transform(event) { + return event; + } + """ + ); } @Override @@ -83,7 +93,7 @@ public class GraalJsScriptEngine implements TransformationEngine { throws ScriptExecutionException { try { Value result = transformFunction.execute(input); - return PolyglotResultConverter.ensureMap(result, language()); + return PolyglotResultConverter.ensureMap(result, metadata().language()); } catch (PolyglotException e) { throw new ScriptExecutionException("Graal JS script execution failed", e); } diff --git a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/ScriptMetadata.java similarity index 76% copy from ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss copy to streampipes-model/src/main/java/org/apache/streampipes/model/connect/ScriptMetadata.java index 8a0f012b14..e6b6633de2 100644 --- a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/ScriptMetadata.java @@ -16,10 +16,12 @@ * */ -.error-color { - color: #963e3e; -} +package org.apache.streampipes.model.connect; + +import org.apache.streampipes.model.shared.annotation.TsModel; -.error-text { - font-size: 12pt; +@TsModel +public record ScriptMetadata(String language, + String name, + String template) { } diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java b/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java index 756750a8e2..e1d6f26905 100644 --- a/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java @@ -17,6 +17,7 @@ */ package org.apache.streampipes.model.extensions.svcdiscovery; +import org.apache.streampipes.model.connect.ScriptMetadata; import org.apache.streampipes.model.extensions.ExtensionItemDescription; import org.apache.streampipes.model.shared.annotation.TsModel; import org.apache.streampipes.model.shared.api.Storable; @@ -24,6 +25,7 @@ import org.apache.streampipes.model.shared.api.Storable; import com.google.gson.annotations.SerializedName; import java.util.HashSet; +import java.util.List; import java.util.Set; @TsModel @@ -43,6 +45,7 @@ public class SpServiceRegistration implements Storable { private String healthCheckPath; private long firstTimeSeenUnhealthy = 0; private SpServiceStatus status = SpServiceStatus.REGISTERED; + private List<ScriptMetadata> supportedScriptLanguages; private Set<ExtensionItemDescription> providedExtensions; @@ -55,6 +58,7 @@ public class SpServiceRegistration implements Storable { String host, int port, Set<SpServiceTag> tags, + List<ScriptMetadata> supportedScriptLanguages, String healthCheckPath, Set<ExtensionItemDescription> providedExtensions) { this.svcType = svcType; @@ -63,6 +67,7 @@ public class SpServiceRegistration implements Storable { this.host = host; this.port = port; this.tags = tags; + this.supportedScriptLanguages = supportedScriptLanguages; this.healthCheckPath = healthCheckPath; this.labels = new HashSet<>(); this.providedExtensions = providedExtensions; @@ -74,9 +79,20 @@ public class SpServiceRegistration implements Storable { String host, Integer port, Set<SpServiceTag> tags, + List<ScriptMetadata> supportedScriptLanguages, String healthCheckPath, Set<ExtensionItemDescription> providedExtensions) { - return new SpServiceRegistration(svcType, svcGroup, svcId, host, port, tags, healthCheckPath, providedExtensions); + return new SpServiceRegistration( + svcType, + svcGroup, + svcId, + host, + port, + tags, + supportedScriptLanguages, + healthCheckPath, + providedExtensions + ); } public int getWeight() { @@ -201,4 +217,9 @@ public class SpServiceRegistration implements Storable { public void setProvidedExtensions(Set<ExtensionItemDescription> providedExtensions) { this.providedExtensions = providedExtensions; } + + public List<ScriptMetadata> getSupportedScriptLanguages() { + return supportedScriptLanguages; + } + } diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java index 5987e769c3..1a59bc8888 100644 --- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java +++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java @@ -35,7 +35,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Stream; public class ExtensionsServiceEndpointGenerator implements IExtensionsServiceEndpointGenerator { @@ -88,7 +87,7 @@ public class ExtensionsServiceEndpointGenerator implements IExtensionsServiceEnd Set<SpServiceTag> customServiceTags) { return SpServiceDiscovery.getServiceDiscovery() .getServiceEndpoints(DefaultSpServiceTypes.EXT, true, - getDesiredServiceTags(appId, spServiceUrlProvider, customServiceTags)); + ExtensionsServiceEndpointUtils.getDesiredServiceTags(appId, spServiceUrlProvider, customServiceTags)); } private String getServiceURL(String appId, SpServiceUrlProvider spServiceUrlProvider, @@ -106,11 +105,4 @@ public class ExtensionsServiceEndpointGenerator implements IExtensionsServiceEnd public static boolean filtersSupported(SpServiceRegistration service, String tag) { return new HashSet<>(service.getTags()).stream().anyMatch(t -> t.asString().equals(tag)); } - - private List<String> getDesiredServiceTags(String appId, SpServiceUrlProvider serviceUrlProvider, - Set<SpServiceTag> customServiceTags) { - return Stream - .concat(Stream.of(serviceUrlProvider.getServiceTag(appId)), customServiceTags.stream()) - .map(SpServiceTag::asString).toList(); - } } diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java index 7c63a315bf..ef4a028ca8 100644 --- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java +++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java @@ -19,6 +19,7 @@ package org.apache.streampipes.manager.execution.endpoint; import org.apache.streampipes.model.base.NamedStreamPipesEntity; import org.apache.streampipes.model.connect.adapter.AdapterDescription; +import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceTag; import org.apache.streampipes.model.graph.DataProcessorDescription; import org.apache.streampipes.model.graph.DataProcessorInvocation; import org.apache.streampipes.model.graph.DataSinkDescription; @@ -26,7 +27,10 @@ import org.apache.streampipes.model.graph.DataSinkInvocation; import org.apache.streampipes.storage.management.StorageDispatcher; import org.apache.streampipes.svcdiscovery.api.model.SpServiceUrlProvider; +import java.util.List; import java.util.NoSuchElementException; +import java.util.Set; +import java.util.stream.Stream; public class ExtensionsServiceEndpointUtils { @@ -51,6 +55,13 @@ public class ExtensionsServiceEndpointUtils { } } + public static List<String> getDesiredServiceTags(String appId, SpServiceUrlProvider serviceUrlProvider, + Set<SpServiceTag> customServiceTags) { + return Stream + .concat(Stream.of(serviceUrlProvider.getServiceTag(appId)), customServiceTags.stream()) + .map(SpServiceTag::asString).toList(); + } + private static boolean isDataProcessor(NamedStreamPipesEntity entity) { return entity instanceof DataProcessorInvocation || entity instanceof DataProcessorDescription; } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/TransformationScriptLanguageResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/TransformationScriptLanguageResource.java new file mode 100644 index 0000000000..5000e107cf --- /dev/null +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/TransformationScriptLanguageResource.java @@ -0,0 +1,66 @@ +/* + * 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. + * + */ + +package org.apache.streampipes.rest.impl.connect; + +import org.apache.streampipes.connect.transformer.api.TransformationEngines; +import org.apache.streampipes.manager.execution.endpoint.ExtensionsServiceEndpointUtils; +import org.apache.streampipes.model.connect.ScriptMetadata; +import org.apache.streampipes.model.connect.adapter.AdapterDescription; +import org.apache.streampipes.svcdiscovery.SpServiceDiscovery; +import org.apache.streampipes.svcdiscovery.api.model.DefaultSpServiceTypes; +import org.apache.streampipes.svcdiscovery.api.model.SpServiceUrlProvider; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v2/connect/master/script-languages") +public class TransformationScriptLanguageResource { + + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List<ScriptMetadata> getAll(@RequestBody AdapterDescription adapterDescription) { + var languagesSupportedByCore = TransformationEngines.INSTANCE.getAvailableEngineMetadata(); + var matchingServices = SpServiceDiscovery.getServiceDiscovery() + .getService(DefaultSpServiceTypes.EXT, true, + ExtensionsServiceEndpointUtils.getDesiredServiceTags( + adapterDescription.getAppId(), + SpServiceUrlProvider.ADAPTER, + adapterDescription.getDeploymentConfiguration() + .getDesiredServiceTags()) + ); + + if (!matchingServices.isEmpty()) { + return matchingServices.get(0) + .getSupportedScriptLanguages() + .stream() + .filter(metadata -> languagesSupportedByCore + .stream() + .anyMatch(coreLanguage -> coreLanguage.language().equals(metadata.language())) + ) + .toList(); + } else { + throw new IllegalArgumentException("No supported languages found"); + } + } +} diff --git a/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java b/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java index afee5cdb74..646ce8ba62 100644 --- a/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java +++ b/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java @@ -46,6 +46,10 @@ public interface ISpServiceDiscovery { List<SpServiceRegistration> getService(boolean restrictToHealthy); + List<SpServiceRegistration> getService(String serviceGroup, + boolean restrictToHealthy, + List<String> filterByTags); + /** * Get all service * @return list of services diff --git a/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java b/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java index ced6b806cd..26deec5803 100644 --- a/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java +++ b/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java @@ -58,7 +58,7 @@ public class SpServiceDiscoveryCore implements ISpServiceDiscovery { } @Override - public List<String> getServiceEndpoints(String serviceGroup, + public List<SpServiceRegistration> getService(String serviceGroup, boolean restrictToHealthy, List<String> filterByTags) { List<SpServiceRegistration> activeServices = findServices(0); @@ -68,10 +68,18 @@ public class SpServiceDiscoveryCore implements ISpServiceDiscovery { .filter(service -> allFiltersSupported(service, filterByTags)) .filter(service -> !restrictToHealthy || service.getStatus() != SpServiceStatus.UNHEALTHY) - .map(this::makeServiceUrl) .collect(Collectors.toList()); } + @Override + public List<String> getServiceEndpoints(String serviceGroup, + boolean restrictToHealthy, + List<String> filterByTags) { + var services = getService(serviceGroup, restrictToHealthy, filterByTags); + + return services.stream().map(this::makeServiceUrl).toList(); + } + @Override public List<SpServiceRegistration> getService(boolean restrictToHealthy) { List<SpServiceRegistration> activeServices = findServices(0); diff --git a/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java b/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java index c11ff8e60f..aa2481afee 100644 --- a/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java +++ b/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java @@ -88,13 +88,15 @@ public abstract class StreamPipesExtensionsServiceBase extends StreamPipesServic serviceDef.setServiceId(serviceId); DeclarersSingleton.getInstance().populate(networkingConfig.getHost(), networkingConfig.getPort(), serviceDef); SpRateLimiter.INSTANCE.createRateLimiter(); - startExtensionsService(this.getClass(), serviceDef, networkingConfig); - ServiceLoadDataReportGenerator.getInstance().initialize(); - registerTransformationEngines(List.of( + registerTransformationEngines(List.of( GroovyScriptEngine::new, GraalJsScriptEngine::new )); + + startExtensionsService(this.getClass(), serviceDef, networkingConfig); + ServiceLoadDataReportGenerator.getInstance().initialize(); + } catch (UnknownHostException e) { LOG.error( "Could not auto-resolve host address - " @@ -129,6 +131,7 @@ public abstract class StreamPipesExtensionsServiceBase extends StreamPipesServic networkingConfig.getHost(), networkingConfig.getPort(), getServiceTags(extensions), + TransformationEngines.INSTANCE.getAvailableEngineMetadata(), getHealthCheckPath(), extensions); diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-languages.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-languages.service.ts new file mode 100644 index 0000000000..73cb673591 --- /dev/null +++ b/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-languages.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { + AdapterDescription, + PlatformServicesCommons, + ScriptMetadata, +} from '@streampipes/platform-services'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class ConnectScriptLanguagesService { + private http = inject(HttpClient); + private platformServicesCommons = inject(PlatformServicesCommons); + + getAll( + adapterDescription: AdapterDescription, + ): Observable<ScriptMetadata[]> { + return this.http.post<ScriptMetadata[]>( + this.baseUrl, + adapterDescription, + ); + } + + get baseUrl(): string { + return `${this.platformServicesCommons.apiBasePath}/connect/master/script-languages`; + } +} diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-templates.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-templates.service.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts index 1d042c59d3..1972c1d960 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts +++ b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ + /* tslint:disable */ /* eslint-disable */ // @ts-nocheck -// Generated using typescript-generator version 3.2.1263 on 2025-12-30 09:52:24. +// Generated using typescript-generator version 3.2.1263 on 2026-01-02 11:22:00. export class NamedStreamPipesEntity implements Storable { '@class': @@ -151,6 +152,9 @@ export class AdapterDescription extends VersionedNamedStreamPipesEntity { } } +/** + * @deprecated since 0.99.0, for removal + */ export class TransformationRuleDescription { '@class': | 'org.apache.streampipes.model.connect.rules.value.ValueTransformationRuleDescription' @@ -224,6 +228,9 @@ export class TransformationRuleDescription { } } +/** + * @deprecated since 0.99.0, for removal + */ export class ValueTransformationRuleDescription extends TransformationRuleDescription { '@class': | 'org.apache.streampipes.model.connect.rules.value.ValueTransformationRuleDescription' @@ -295,6 +302,9 @@ export class AddTimestampRuleDescription extends ValueTransformationRuleDescript } } +/** + * @deprecated since 0.99.0, for removal + */ export class AddValueTransformationRuleDescription extends ValueTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.value.AddValueTransformationRuleDescription'; 'datatype': string; @@ -757,6 +767,9 @@ export class Certificate implements Storable { } } +/** + * @deprecated since 0.99.0, for removal + */ export class ChangeDatatypeTransformationRuleDescription extends ValueTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.value.ChangeDatatypeTransformationRuleDescription'; 'originalDatatypeXsd': string; @@ -1800,6 +1813,9 @@ export class EventPropertyPrimitive extends EventProperty { } } +/** + * @deprecated since 0.99.0, for removal + */ export class StreamTransformationRuleDescription extends TransformationRuleDescription { '@class': | 'org.apache.streampipes.model.connect.rules.stream.StreamTransformationRuleDescription' @@ -1835,6 +1851,9 @@ export class StreamTransformationRuleDescription extends TransformationRuleDescr } } +/** + * @deprecated since 0.99.0, for removal + */ export class EventRateTransformationRuleDescription extends StreamTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.stream.EventRateTransformationRuleDescription'; 'aggregationTimeWindow': number; @@ -3424,6 +3443,9 @@ export class RemoveDuplicateRule { } } +/** + * @deprecated since 0.99.0, for removal + */ export class RemoveDuplicatesTransformationRuleDescription extends StreamTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.stream.RemoveDuplicatesTransformationRuleDescription'; 'filterTimeWindow': string; @@ -3744,6 +3766,26 @@ export class SampleData { } } +export class ScriptMetadata { + language: string; + name: string; + template: string; + + static fromData( + data: ScriptMetadata, + target?: ScriptMetadata, + ): ScriptMetadata { + if (!data) { + return data; + } + const instance = target || new ScriptMetadata(); + instance.language = data.language; + instance.name = data.name; + instance.template = data.template; + return instance; + } +} + export class SecretStaticProperty extends StaticProperty { '@class': 'org.apache.streampipes.model.staticproperty.SecretStaticProperty'; 'encrypted': boolean; @@ -4376,6 +4418,9 @@ export class TreeInputNode { } } +/** + * @deprecated since 0.99.0, for removal + */ export class UnitTransformRuleDescription extends ValueTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.value.UnitTransformRuleDescription'; 'fromUnitRessourceURL': string; diff --git a/ui/projects/streampipes/platform-services/src/public-api.ts b/ui/projects/streampipes/platform-services/src/public-api.ts index 2eed5bd462..0897190af8 100644 --- a/ui/projects/streampipes/platform-services/src/public-api.ts +++ b/ui/projects/streampipes/platform-services/src/public-api.ts @@ -29,6 +29,7 @@ export * from './lib/apis/asset-management.service'; export * from './lib/apis/compact-pipeline.service'; export * from './lib/apis/certificate.service'; export * from './lib/apis/chart.service'; +export * from './lib/apis/connect-script-languages.service'; export * from './lib/apis/dashboard.service'; export * from './lib/apis/dashboard-kiosk.service'; export * from './lib/apis/datalake-rest.service'; 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 1b1cb0ad17..11c92c62e1 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 @@ -110,7 +110,11 @@ export class AdapterConfigurationStateService { // 3. Automatically run the script after getting the sample const currentScript = updatedAdapter.transformationConfig.script; - this.runScript(updatedAdapter, currentScript); + this.runScript( + updatedAdapter, + currentScript, + updatedAdapter.transformationConfig.language, + ); }, error: (error: HttpErrorResponse) => { // Update state with error AND metadata (error/idle) @@ -122,7 +126,11 @@ export class AdapterConfigurationStateService { }); } - public runScript(adapter: AdapterDescription, script: string): void { + public runScript( + adapter: AdapterDescription, + script: string, + language: string, + ): void { // 1. Prepare state for loading this.updateState({ isRunningScript: true, @@ -132,7 +140,7 @@ export class AdapterConfigurationStateService { // 2. Update the local adapter object with the latest script from the UI const updatedAdapter = { ...adapter }; updatedAdapter.transformationConfig.script = script; - updatedAdapter.transformationConfig.language = 'javascript'; + updatedAdapter.transformationConfig.language = language; // 3. Execute the API call this.restService.sampleTransform(updatedAdapter).subscribe({ diff --git a/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss b/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss index b4c4b08df6..c4108bbd62 100644 --- a/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss +++ b/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss @@ -18,11 +18,11 @@ .preview-text { margin: var(--space-2xs); - background-color: var(--color-code-bg); + background-color: var(--color-bg-0); font: var(--font-size-xs) Inconsolata, monospace; - color: white; + color: var(--color-default-text); padding: 10px; max-width: 100%; max-height: 300px; diff --git a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html index 70f5c17c8c..3f1eafffbd 100644 --- a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html +++ b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html @@ -18,14 +18,17 @@ <div fxLayout="column" fxFlex="100"> <div fxLayout="row" fxLayoutAlign="center center" fxFlex="100"> - <div fxLayoutAlign="start center" class="error-text"> + <div fxLayoutAlign="start center" class="error-text mt-10"> {{ 'There was an error while guessing the schema of your configured data stream' | translate }}: </div> </div> - <div fxLayout="row" fxLayoutAlign="center center" class="mt-10"> - <sp-exception-message [message]="errorMessage"></sp-exception-message> + <div fxLayout="row" fxLayoutAlign="center center" class="mt-10 w-100"> + <sp-exception-message + [message]="errorMessage" + class="w-100" + ></sp-exception-message> </div> </div> diff --git a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss index 8a0f012b14..ac0cecd92a 100644 --- a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss +++ b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss @@ -21,5 +21,5 @@ } .error-text { - font-size: 12pt; + font-size: var(--font-size-md); } 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 564eb2780a..c7d1f9feb3 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 @@ -18,20 +18,52 @@ <div fxFlex="100" fxLayout="column"> <div fxFlex="100" fxLayout="column"> <sp-basic-inner-panel - innerPadding="0" - [panelTitle]="'Basic Settings' | translate" + [panelTitle]="'Transformation' | translate" outerMargin="20px 0px" > - <div header fxLayoutAlign="end center" fxFlex="100"> + <div + header + fxLayoutAlign="end center" + fxFlex="100" + fxLayoutGap="10px" + > + @if (selectedScriptMetadata() !== undefined) { + <button + mat-button + [matMenuTriggerFor]="langMenu" + aria-label="Select template language" + > + {{ selectedScriptMetadata().name | titlecase }} + <mat-icon>arrow_drop_down</mat-icon> + </button> + + <mat-menu #langMenu="matMenu"> + @for ( + script of availableScripts; + track script.language + ) { + <button + mat-menu-item + (click)="onLanguageChange(script)" + > + <span>{{ script.name | titlecase }}</span> + @if ( + script.language === + selectedScriptMetadata().language + ) { + <mat-icon class="ms-auto">check</mat-icon> + } + </button> + } + </mat-menu> + } <button - color="accent" mat-button - matTooltip="Run script" - data-cy="configure-schema-run-script-button" - (click)="runScript()" + data-cy="reset-script" + (click)="resetScript()" > - <mat-icon>play_circle_filled</mat-icon> - <span>Run script</span> + <mat-icon>settings_backup_restore</mat-icon> + <span>Reset script</span> </button> </div> <div class="code-editor-outer"> @@ -43,6 +75,28 @@ data-cy="configure-schema-script-editor" ></ngx-codemirror> </div> + <div fxLayoutGap="10px"> + <button + class="mt-sm" + mat-flat-button + matTooltip="Run script" + data-cy="configure-schema-run-script-button" + (click)="runScript()" + > + <mat-icon>play_circle_filled</mat-icon> + <span>Run script</span> + </button> + <button + class="mat-basic mt-sm" + mat-flat-button + matTooltip="Run script" + data-cy="add-script-template-button" + (click)="runScript()" + > + <mat-icon>play_circle_filled</mat-icon> + <span>Create script template</span> + </button> + </div> </sp-basic-inner-panel> </div> @@ -56,12 +110,11 @@ <div header fxLayoutAlign="end center" fxFlex="100"> <button data-cy="connect-get-new-sample-button" - color="accent" mat-button - matTooltip="Get New Sample" (click)="getSampleEvent()" > <mat-icon>refresh</mat-icon> + <span>{{ 'Get new sample' | translate }}</span> </button> </div> @if (isSampleLoading()) { diff --git a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss index b6331428c8..5010c7939e 100644 --- a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss +++ b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss @@ -30,10 +30,10 @@ .preview-text { margin: var(--space-2xs); - background-color: var(--color-code-bg); + background-color: var(--color-bg-0); font: var(--font-size-xs) Inconsolata, monospace; - color: white; + color: var(--color-default-text); padding: 10px; } 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 794225d259..dd8a7ae5de 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 @@ -27,7 +27,11 @@ import { signal, } from '@angular/core'; import { MatStepper } from '@angular/material/stepper'; -import { AdapterDescription } from '@streampipes/platform-services'; +import { + AdapterDescription, + ConnectScriptLanguagesService, + ScriptMetadata, +} from '@streampipes/platform-services'; import { AdapterConfigurationStateService } from '../adapter-configuration-state-service/adapter-configuration-state.service'; @Component({ @@ -38,6 +42,7 @@ import { AdapterConfigurationStateService } from '../adapter-configuration-state }) export class ConfigureSchemaComponent implements OnInit { private stateService = inject(AdapterConfigurationStateService); + private scriptLanguagesService = inject(ConnectScriptLanguagesService); @Input() adapterDescription: AdapterDescription; @@ -51,6 +56,8 @@ export class ConfigureSchemaComponent implements OnInit { @Output() nextEmitter: EventEmitter<MatStepper> = new EventEmitter(); + availableScripts: ScriptMetadata[] = []; + isSampleLoading = computed(() => this.stateService.state().isGettingSample); sampleErrorMessage = computed(() => this.stateService.state().sampleError); @@ -70,10 +77,9 @@ export class ConfigureSchemaComponent implements OnInit { ?.outputs?.[0] || {}, ); - script = signal(`// returns the same event -function transform(event) { - return event; -}`); + script = signal(undefined); + initialScript = signal<{ meta: ScriptMetadata; script: string }>(undefined); + selectedScriptMetadata = signal(undefined); editorOptions = { mode: 'javascript', @@ -94,25 +100,64 @@ function transform(event) { } private initializeScriptVariable(): void { - const currentScript = - this.adapterDescription.transformationConfig.script; - if (currentScript) { - this.script.set(currentScript); - } else { - this.adapterDescription.transformationConfig.script = this.script(); - } + this.scriptLanguagesService + .getAll(this.adapterDescription) + .subscribe(res => { + this.availableScripts = res; + const currentScript = + this.adapterDescription.transformationConfig.script; + let meta: ScriptMetadata; + if (currentScript) { + this.script.set(currentScript); + meta = this.availableScripts.find( + s => + s.language === + this.adapterDescription.transformationConfig + .language, + ); + this.initialScript.set({ meta, script: currentScript }); + } else { + meta = this.availableScripts.find( + s => s.language === 'javascript', + ); + this.adapterDescription.transformationConfig.script = + meta.template; + this.adapterDescription.transformationConfig.language = + meta.language; + this.script.set(meta.template); + } + this.selectedScriptMetadata.set(meta); + }); } onCodeChange(newCode: string) { this.script.set(newCode); } + onLanguageChange(newLanguage: ScriptMetadata) { + this.selectedScriptMetadata.set(newLanguage); + this.script.set(newLanguage.template); + } + + resetScript(): void { + if (this.initialScript() !== undefined) { + this.script.set(this.initialScript().script); + this.selectedScriptMetadata.set(this.initialScript().meta); + } else { + this.script.set(this.selectedScriptMetadata().template); + } + } + getSampleEvent(): void { this.stateService.getSampleEvent(this.adapterDescription); } runScript(): void { - this.stateService.runScript(this.adapterDescription, this.script()); + this.stateService.runScript( + this.adapterDescription, + this.script(), + this.selectedScriptMetadata().language, + ); } public cancel() {
