This is an automated email from the ASF dual-hosted git repository.
dominikriemer 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 cc7418a9ac Add running instance dialog registered services (#4700)
cc7418a9ac is described below
commit cc7418a9ac770e707939a4bea5e403491e7714f1
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Jul 9 08:23:06 2026 +0200
Add running instance dialog registered services (#4700)
---
.../ExtensionInstanceDetailsProvider.java | 170 ++++++++
.../ExtensionInstanceRemovalService.java | 197 +++++++++
.../monitoring/model/RunningAdapterInstance.java | 28 ++
.../model/RunningExtensionInstances.java | 26 ++
.../model/RunningPipelineElementInstance.java | 28 ++
.../impl/admin/ServiceRegistrationResource.java | 77 ++++
ui/deployment/i18n/de.json | 8 +-
ui/deployment/i18n/en.json | 8 +-
ui/deployment/i18n/pl.json | 8 +-
...xtensions-service-details-dialog.component.html | 2 +-
...service-running-instances-dialog.component.html | 454 +++++++++++++++++++++
...service-running-instances-dialog.component.scss | 48 +++
...s-service-running-instances-dialog.component.ts | 375 +++++++++++++++++
.../registered-extensions-services.component.html | 16 +-
.../registered-extensions-services.component.ts | 17 +-
.../configuration/shared/configuration.service.ts | 66 +++
16 files changed, 1504 insertions(+), 24 deletions(-)
diff --git
a/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/ExtensionInstanceDetailsProvider.java
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/ExtensionInstanceDetailsProvider.java
new file mode 100644
index 0000000000..8aef254bec
--- /dev/null
+++
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/ExtensionInstanceDetailsProvider.java
@@ -0,0 +1,170 @@
+/*
+ * 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.health.monitoring;
+
+import org.apache.streampipes.commons.constants.InstanceIdExtractor;
+import org.apache.streampipes.health.monitoring.model.ActiveResources;
+import org.apache.streampipes.health.monitoring.model.RunningAdapterInstance;
+import
org.apache.streampipes.health.monitoring.model.RunningExtensionInstances;
+import
org.apache.streampipes.health.monitoring.model.RunningPipelineElementInstance;
+import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
+import org.apache.streampipes.model.base.InvocableStreamPipesEntity;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.graph.DataProcessorInvocation;
+import org.apache.streampipes.model.graph.DataSinkInvocation;
+import org.apache.streampipes.model.health.ExtensionInstanceHealth;
+import org.apache.streampipes.model.pipeline.Pipeline;
+import org.apache.streampipes.resource.management.SpResourceManager;
+import org.apache.streampipes.storage.api.system.IExtensionsServiceStorage;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class ExtensionInstanceDetailsProvider {
+
+ private final IExtensionsServiceStorage extensionsServiceStorage;
+ private final ResourceProvider resourceProvider;
+ private final ExtensionServiceRequestManager extensionRequestManager;
+ private final SpResourceManager resourceManager;
+
+ public ExtensionInstanceDetailsProvider(IExtensionsServiceStorage
extensionsServiceStorage,
+ ResourceProvider resourceProvider,
+ ExtensionServiceRequestManager
extensionRequestManager,
+ SpResourceManager resourceManager) {
+ this.extensionsServiceStorage = extensionsServiceStorage;
+ this.resourceProvider = resourceProvider;
+ this.extensionRequestManager = extensionRequestManager;
+ this.resourceManager = resourceManager;
+ }
+
+ public RunningExtensionInstances getRunningInstances(String serviceId) {
+ var extensionHealth = new ExtensionInstanceAvailabilityCheck(
+ extensionsServiceStorage,
+ serviceId,
+ extensionRequestManager,
+ resourceManager
+ ).checkRunningInstances();
+
+ var activeResources = resourceProvider.loadActiveResources();
+ return new RunningExtensionInstances(
+ serviceId,
+ getRunningAdapters(serviceId, extensionHealth, activeResources),
+ getRunningPipelineElements(serviceId, extensionHealth, activeResources)
+ );
+ }
+
+ private List<RunningAdapterInstance> getRunningAdapters(String serviceId,
+
ExtensionInstanceHealth extensionHealth,
+ ActiveResources
activeResources) {
+ Map<String, AdapterDescription> adaptersByInstanceId =
activeResources.allAdapters()
+ .stream()
+ .filter(adapter -> serviceId.equals(adapter.getSelectedServiceId()))
+ .collect(Collectors.toMap(
+ AdapterDescription::getElementId,
+ adapter -> adapter,
+ (existing, replacement) -> existing
+ ));
+
+ return extensionHealth.adapterInstanceStates()
+ .entrySet()
+ .stream()
+ .map(entry -> {
+ var adapter = adaptersByInstanceId.get(entry.getKey());
+ return new RunningAdapterInstance(
+ entry.getKey(),
+ adapter != null ? adapter.getName() : null,
+ adapter != null ? adapter.getAppId() : null,
+ entry.getValue(),
+ adapter == null
+ );
+ })
+ .sorted(Comparator
+ .comparing(RunningAdapterInstance::orphaned).reversed()
+ .thenComparing(RunningAdapterInstance::name,
+ Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))
+ .thenComparing(RunningAdapterInstance::instanceId))
+ .toList();
+ }
+
+ private List<RunningPipelineElementInstance>
getRunningPipelineElements(String serviceId,
+
ExtensionInstanceHealth extensionHealth,
+
ActiveResources activeResources) {
+ Map<String, RunningPipelineElementInstance> pipelineElementsByInstanceId =
activeResources.runningPipelines()
+ .stream()
+ .flatMap(pipeline -> runningPipelineElements(pipeline)
+ .filter(element ->
serviceId.equals(element.getSelectedServiceId()))
+ .map(element -> new RunningPipelineElementInstance(
+ InstanceIdExtractor.extractId(element.getElementId()),
+ element.getName(),
+ element.getAppId(),
+ pipeline.getPipelineId(),
+ pipeline.getName(),
+ getPipelineElementType(element),
+ false
+ )))
+ .collect(Collectors.toMap(
+ RunningPipelineElementInstance::instanceId,
+ element -> element,
+ (existing, replacement) -> existing
+ ));
+
+ return extensionHealth.runningPipelineElementInstanceIds()
+ .stream()
+ .map(instanceId -> pipelineElementsByInstanceId.getOrDefault(
+ instanceId,
+ new RunningPipelineElementInstance(
+ instanceId,
+ null,
+ null,
+ null,
+ null,
+ "UNKNOWN",
+ true
+ )
+ ))
+ .sorted(Comparator
+ .comparing(RunningPipelineElementInstance::orphaned).reversed()
+ .thenComparing(RunningPipelineElementInstance::pipelineName,
+ Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))
+ .thenComparing(RunningPipelineElementInstance::name,
+ Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))
+ .thenComparing(RunningPipelineElementInstance::instanceId))
+ .toList();
+ }
+
+ private Stream<InvocableStreamPipesEntity> runningPipelineElements(Pipeline
pipeline) {
+ return Stream.concat(
+ pipeline.getSepas().stream(),
+ pipeline.getActions().stream()
+ );
+ }
+
+ private String getPipelineElementType(InvocableStreamPipesEntity element) {
+ if (element instanceof DataProcessorInvocation) {
+ return "PROCESSOR";
+ } else if (element instanceof DataSinkInvocation) {
+ return "SINK";
+ } else {
+ return "UNKNOWN";
+ }
+ }
+}
diff --git
a/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/ExtensionInstanceRemovalService.java
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/ExtensionInstanceRemovalService.java
new file mode 100644
index 0000000000..09a37c86eb
--- /dev/null
+++
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/ExtensionInstanceRemovalService.java
@@ -0,0 +1,197 @@
+/*
+ * 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.health.monitoring;
+
+import org.apache.streampipes.commons.constants.InstanceIdExtractor;
+import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
+import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestTargets;
+import org.apache.streampipes.manager.api.extensions.ExtensionServiceRequests;
+import org.apache.streampipes.model.base.InvocableStreamPipesEntity;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import
org.apache.streampipes.model.extensions.svcdiscovery.SpServiceRegistration;
+import org.apache.streampipes.model.graph.DataSinkInvocation;
+import org.apache.streampipes.model.pipeline.Pipeline;
+import org.apache.streampipes.resource.management.SpResourceManager;
+import org.apache.streampipes.serializers.json.JacksonSerializer;
+import org.apache.streampipes.storage.api.system.IExtensionsServiceStorage;
+import org.apache.streampipes.svcdiscovery.api.model.SpServiceUrlProvider;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class ExtensionInstanceRemovalService {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ExtensionInstanceRemovalService.class);
+
+ private final IExtensionsServiceStorage extensionsServiceStorage;
+ private final ResourceProvider resourceProvider;
+ private final ExtensionServiceRequestManager extensionRequestManager;
+ private final SpResourceManager resourceManager;
+
+ public ExtensionInstanceRemovalService(IExtensionsServiceStorage
extensionsServiceStorage,
+ ResourceProvider resourceProvider,
+ ExtensionServiceRequestManager
extensionRequestManager,
+ SpResourceManager resourceManager) {
+ this.extensionsServiceStorage = extensionsServiceStorage;
+ this.resourceProvider = resourceProvider;
+ this.extensionRequestManager = extensionRequestManager;
+ this.resourceManager = resourceManager;
+ }
+
+ public void removeAllInstances(String serviceId) throws IOException {
+ var runningInstances = new ExtensionInstanceDetailsProvider(
+ extensionsServiceStorage,
+ resourceProvider,
+ extensionRequestManager,
+ resourceManager
+ ).getRunningInstances(serviceId);
+
+ for (var adapter : runningInstances.adapters()) {
+ try {
+ removeAdapterInstance(serviceId, adapter.instanceId());
+ } catch (IOException e) {
+ LOG.warn("Could not remove adapter instance {} from service {} during
bulk removal",
+ adapter.instanceId(), serviceId, e);
+ }
+ }
+
+ for (var pipelineElement : runningInstances.pipelineElements()) {
+ try {
+ removePipelineElementInstance(serviceId, pipelineElement.instanceId());
+ } catch (IOException e) {
+ LOG.warn("Could not remove pipeline element instance {} from service
{} during bulk removal",
+ pipelineElement.instanceId(), serviceId, e);
+ }
+ }
+ }
+
+ public void removeAdapterInstance(String serviceId,
+ String instanceId) throws IOException {
+ var service = getService(serviceId);
+ var activeResources = resourceProvider.loadActiveResources();
+ var adapter = activeResources.allAdapters()
+ .stream()
+ .filter(candidate ->
serviceId.equals(candidate.getSelectedServiceId()))
+ .filter(candidate -> instanceId.equals(candidate.getElementId()))
+ .findFirst()
+ .map(AdapterDescription::new)
+ .orElseGet(() -> makeOrphanedAdapterDescription(instanceId));
+
+ adapter.setRunning(true);
+
+ var requestTarget = ExtensionServiceRequestTargets.adapterStop(service);
+ var response = extensionRequestManager.request(
+ ExtensionServiceRequests.adapterStateChange(
+ requestTarget,
+ instanceId,
+ serialize(adapter),
+ resourceManager
+ )
+ );
+
+ if (!response.isSuccess()) {
+ throw new IOException("Could not remove adapter instance %s from service
%s: %s"
+ .formatted(instanceId, serviceId, response.responseBody()));
+ }
+ }
+
+ public void removePipelineElementInstance(String serviceId,
+ String instanceId) throws
IOException {
+ var service = getService(serviceId);
+ var activeResources = resourceProvider.loadActiveResources();
+ var match = activeResources.runningPipelines()
+ .stream()
+ .flatMap(pipeline -> runningPipelineElements(pipeline)
+ .filter(element ->
serviceId.equals(element.getSelectedServiceId()))
+ .filter(element ->
instanceId.equals(InstanceIdExtractor.extractId(element.getElementId())))
+ .map(element -> new PipelineElementRemovalTarget(
+ element.getAppId(),
+ pipeline.getPipelineId(),
+ getProvider(element)
+ )))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElseGet(() -> new PipelineElementRemovalTarget(
+ instanceId,
+ null,
+ SpServiceUrlProvider.DATA_PROCESSOR
+ ));
+
+ var requestTarget = ExtensionServiceRequestTargets.pipelineDetach(
+ service.getServiceUrl(),
+ service.getSvcId(),
+ match.provider(),
+ match.appId(),
+ instanceId
+ );
+ var response = extensionRequestManager.request(
+ ExtensionServiceRequests.pipelineElementDetach(
+ requestTarget,
+ match.pipelineId(),
+ resourceManager
+ )
+ );
+
+ if (!response.isSuccess()) {
+ throw new IOException("Could not remove pipeline element instance %s
from service %s: %s"
+ .formatted(instanceId, serviceId, response.responseBody()));
+ }
+ }
+
+ private SpServiceRegistration getService(String serviceId) throws
IOException {
+ return extensionsServiceStorage.findAll()
+ .stream()
+ .filter(service -> serviceId.equals(service.getSvcId()))
+ .findFirst()
+ .orElseThrow(() -> new IOException("Could not find extension service "
+ serviceId));
+ }
+
+ private AdapterDescription makeOrphanedAdapterDescription(String instanceId)
{
+ var adapterDescription = new AdapterDescription();
+ adapterDescription.setElementId(instanceId);
+ return adapterDescription;
+ }
+
+ private String serialize(AdapterDescription adapterDescription) throws
JsonProcessingException {
+ return
JacksonSerializer.getObjectMapper().writeValueAsString(adapterDescription);
+ }
+
+ private Stream<InvocableStreamPipesEntity> runningPipelineElements(Pipeline
pipeline) {
+ return Stream.concat(
+ pipeline.getSepas().stream(),
+ pipeline.getActions().stream()
+ );
+ }
+
+ private SpServiceUrlProvider getProvider(InvocableStreamPipesEntity element)
{
+ return element instanceof DataSinkInvocation
+ ? SpServiceUrlProvider.DATA_SINK
+ : SpServiceUrlProvider.DATA_PROCESSOR;
+ }
+
+ private record PipelineElementRemovalTarget(String appId,
+ String pipelineId,
+ SpServiceUrlProvider provider) {
+ }
+}
diff --git
a/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningAdapterInstance.java
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningAdapterInstance.java
new file mode 100644
index 0000000000..1f90ab3da1
--- /dev/null
+++
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningAdapterInstance.java
@@ -0,0 +1,28 @@
+/*
+ * 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.health.monitoring.model;
+
+import org.apache.streampipes.model.health.AdapterInstanceState;
+
+public record RunningAdapterInstance(String instanceId,
+ String name,
+ String appId,
+ AdapterInstanceState state,
+ boolean orphaned) {
+}
diff --git
a/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningExtensionInstances.java
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningExtensionInstances.java
new file mode 100644
index 0000000000..007f6fd0f2
--- /dev/null
+++
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningExtensionInstances.java
@@ -0,0 +1,26 @@
+/*
+ * 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.health.monitoring.model;
+
+import java.util.List;
+
+public record RunningExtensionInstances(String serviceId,
+ List<RunningAdapterInstance> adapters,
+ List<RunningPipelineElementInstance>
pipelineElements) {
+}
diff --git
a/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningPipelineElementInstance.java
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningPipelineElementInstance.java
new file mode 100644
index 0000000000..4a46ba5ae1
--- /dev/null
+++
b/streampipes-health-monitoring/src/main/java/org/apache/streampipes/health/monitoring/model/RunningPipelineElementInstance.java
@@ -0,0 +1,28 @@
+/*
+ * 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.health.monitoring.model;
+
+public record RunningPipelineElementInstance(String instanceId,
+ String name,
+ String appId,
+ String pipelineId,
+ String pipelineName,
+ String type,
+ boolean orphaned) {
+}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/ServiceRegistrationResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/ServiceRegistrationResource.java
index ce19d8b6db..eadb100e3d 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/ServiceRegistrationResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/ServiceRegistrationResource.java
@@ -17,10 +17,16 @@
*/
package org.apache.streampipes.rest.impl.admin;
+import
org.apache.streampipes.health.monitoring.ExtensionInstanceDetailsProvider;
+import
org.apache.streampipes.health.monitoring.ExtensionInstanceRemovalService;
+import org.apache.streampipes.health.monitoring.ResourceProvider;
import org.apache.streampipes.health.monitoring.ServiceRegistrationManager;
+import
org.apache.streampipes.health.monitoring.model.RunningExtensionInstances;
+import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
import
org.apache.streampipes.model.extensions.svcdiscovery.SpServiceRegistration;
import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceStatus;
import org.apache.streampipes.model.message.Notifications;
+import org.apache.streampipes.resource.management.SpResourceManager;
import
org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
import org.apache.streampipes.rest.security.AuthConstants;
import org.apache.streampipes.rest.shared.exception.SpMessageException;
@@ -30,6 +36,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -37,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.io.IOException;
import java.util.List;
@RestController
@@ -46,12 +54,64 @@ public class ServiceRegistrationResource extends
AbstractAuthGuardedRestResource
private final IExtensionsServiceStorage extensionsServiceStorage =
getNoSqlStorage().getExtensionsServiceStorage();
+ private final ExtensionServiceRequestManager extensionServiceRequestManager;
+ private final SpResourceManager resourceManager;
+
+ public ServiceRegistrationResource(ExtensionServiceRequestManager
extensionServiceRequestManager,
+ SpResourceManager resourceManager) {
+ this.extensionServiceRequestManager = extensionServiceRequestManager;
+ this.resourceManager = resourceManager;
+ }
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<SpServiceRegistration>> getRegisteredServices() {
return ok(extensionsServiceStorage.findAll());
}
+ @GetMapping(path = "/{serviceId}/running-instances", produces =
MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<RunningExtensionInstances>
getRunningInstances(@PathVariable("serviceId") String serviceId) {
+ return ok(new ExtensionInstanceDetailsProvider(
+ extensionsServiceStorage,
+ makeResourceProvider(),
+ extensionServiceRequestManager,
+ resourceManager
+ ).getRunningInstances(serviceId));
+ }
+
+ @DeleteMapping(path = "/{serviceId}/running-instances", produces =
MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<Void>
removeAllRunningInstances(@PathVariable("serviceId") String serviceId) {
+ try {
+ makeRemovalService().removeAllInstances(serviceId);
+ return ok();
+ } catch (IOException e) {
+ throw new SpMessageException(HttpStatus.BAD_REQUEST,
Notifications.error(e.getMessage()));
+ }
+ }
+
+ @DeleteMapping(path = "/{serviceId}/running-instances/adapters/{instanceId}",
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<Void> removeAdapterInstance(@PathVariable("serviceId")
String serviceId,
+
@PathVariable("instanceId") String instanceId) {
+ try {
+ makeRemovalService().removeAdapterInstance(serviceId, instanceId);
+ return ok();
+ } catch (IOException e) {
+ throw new SpMessageException(HttpStatus.BAD_REQUEST,
Notifications.error(e.getMessage()));
+ }
+ }
+
+ @DeleteMapping(path =
"/{serviceId}/running-instances/pipeline-elements/{instanceId}",
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity<Void>
removePipelineElementInstance(@PathVariable("serviceId") String serviceId,
+
@PathVariable("instanceId") String instanceId) {
+ try {
+ makeRemovalService().removePipelineElementInstance(serviceId,
instanceId);
+ return ok();
+ } catch (IOException e) {
+ throw new SpMessageException(HttpStatus.BAD_REQUEST,
Notifications.error(e.getMessage()));
+ }
+ }
+
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> registerService(@RequestBody
SpServiceRegistration serviceRegistration) {
new
ServiceRegistrationManager(extensionsServiceStorage).addService(serviceRegistration,
@@ -69,4 +129,21 @@ public class ServiceRegistrationResource extends
AbstractAuthGuardedRestResource
Notifications.error("Could not find registered service with id " +
serviceId));
}
}
+
+ private ResourceProvider makeResourceProvider() {
+ return new ResourceProvider(
+ resourceManager.managePipelines().getDb(),
+ resourceManager.manageAdapters().getDb(),
+ null
+ );
+ }
+
+ private ExtensionInstanceRemovalService makeRemovalService() {
+ return new ExtensionInstanceRemovalService(
+ extensionsServiceStorage,
+ makeResourceProvider(),
+ extensionServiceRequestManager,
+ resourceManager
+ );
+ }
}
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 5f37866fe6..9a5288c723 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -221,7 +221,6 @@
"Configure labels which can be assigned to assets and other resources":
"Labels konfigurieren, die Assets und anderen Ressourcen zugewiesen werden
können",
"Configure schema": "Schema konfigurieren",
"Configure trusted and rejected OPC-UA certificates": "Konfiguration der
vertrauenswürdigen und abgelehnten OPC-UA Zertifikate",
- "Configured": "Konfiguriert",
"Confirm password": "Passwort bestätigen",
"Connect Adapter": "Adapter anschließen",
"Connection could not be established.": "Die Verbindung konnte nicht
hergestellt werden.",
@@ -335,13 +334,13 @@
"Delete the dataset entirely. All historical data is lost!": "Löschen Sie
den Datensatz vollständig. Alle historischen Daten gehen verloren!",
"Delete user": "Benutzer löschen",
"Deleted fields": "Gelöschte Felder",
+ "Deleted instances of active pipelines or adapters will be redeployed by the
health check.": "Gelöschte Instanzen aktiver Pipelines oder Adapter werden
durch den Health Check erneut bereitgestellt.",
"Deleting data...": "Daten löschen...",
"Deleting pipeline...": "Löschen der Pipeline...",
"Delimiter": "Trennzeichen",
"Delta font size": "Delta-Schriftgröße",
"Depending on number and size of exported files, this might take a
while...": "Je nach Anzahl und Größe der exportierten Dateien kann dies eine
Weile dauern...",
"Description": "Beschreibung",
- "Deselect All": "Alle abwählen",
"Deselect all": "Alle abwählen",
"Details": "Einzelheiten",
"Dialog Title": "Dialogfeld",
@@ -773,9 +772,6 @@
"Order": "Sortieren",
"Original": "Original",
"Original (Parsed)": "Original (geparst)",
- "Orphaned": "Verwaist",
- "Orphaned adapter instance": "Verwaiste Adapterinstanz",
- "Orphaned pipeline element instance": "Verwaiste Pipeline-Element-Instanz",
"Others Label": "Beschriftung für Sonstige",
"Output Topics": "Output-Topics",
"Output stream": "Ausgabestream",
@@ -863,6 +859,7 @@
"Refresh charts": "Diagramme aktualisieren",
"Refresh interval": "Aktualisierungsintervall",
"Refresh metrics": "Metriken aktualisieren",
+ "Refresh running instances": "Laufende Instanzen aktualisieren",
"Region": "Region",
"Register": "Registrieren",
"Registered services": "Registrierte Services",
@@ -956,7 +953,6 @@
"Second": "Sekunde",
"Secret Key": "Secret Key",
"Select Adapter": "Adapter auswählen",
- "Select All": "Alle auswählen",
"Select CSV file": "CSV-Datei auswählen",
"Select Data": "Daten auswählen",
"Select Fields": "Felder auswählen",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 20f4d098be..a2a0063823 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -221,7 +221,6 @@
"Configure labels which can be assigned to assets and other resources": null,
"Configure schema": null,
"Configure trusted and rejected OPC-UA certificates": null,
- "Configured": null,
"Confirm password": null,
"Connect Adapter": null,
"Connection could not be established.": null,
@@ -335,13 +334,13 @@
"Delete the dataset entirely. All historical data is lost!": null,
"Delete user": null,
"Deleted fields": null,
+ "Deleted instances of active pipelines or adapters will be redeployed by the
health check.": null,
"Deleting data...": null,
"Deleting pipeline...": null,
"Delimiter": null,
"Delta font size": null,
"Depending on number and size of exported files, this might take a
while...": null,
"Description": null,
- "Deselect All": null,
"Deselect all": null,
"Details": null,
"Dialog Title": null,
@@ -773,9 +772,6 @@
"Order": null,
"Original": null,
"Original (Parsed)": null,
- "Orphaned": null,
- "Orphaned adapter instance": null,
- "Orphaned pipeline element instance": null,
"Others Label": null,
"Output Topics": null,
"Output stream": null,
@@ -863,6 +859,7 @@
"Refresh charts": null,
"Refresh interval": null,
"Refresh metrics": null,
+ "Refresh running instances": null,
"Region": null,
"Register": null,
"Registered services": null,
@@ -956,7 +953,6 @@
"Second": null,
"Secret Key": null,
"Select Adapter": null,
- "Select All": null,
"Select CSV file": null,
"Select Data": null,
"Select Fields": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index c548ab39cd..a54c8b4a4a 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -221,7 +221,6 @@
"Configure labels which can be assigned to assets and other resources":
"Skonfiguruj etykiety, które mogą być przypisywane do zasobów i innych zasobów",
"Configure schema": "Skonfiguruj schemat",
"Configure trusted and rejected OPC-UA certificates": "Skonfiguruj zaufane i
odrzucone certyfikaty OPC-UA",
- "Configured": "Skonfigurowane",
"Confirm password": "Potwierdź hasło",
"Connect Adapter": "Podłącz adapter",
"Connection could not be established.": "Nie udało się nawiązać połączenia.",
@@ -335,13 +334,13 @@
"Delete the dataset entirely. All historical data is lost!": "Usuń cały
zestaw danych. Wszystkie dane historyczne zostaną utracone!",
"Delete user": "Usuń użytkownika",
"Deleted fields": "Usunięte pola",
+ "Deleted instances of active pipelines or adapters will be redeployed by the
health check.": "Usunięte instancje aktywnych strumieni lub adapterów zostaną
ponownie wdrożone przez mechanizm sprawdzania stanu.",
"Deleting data...": "Usuwanie danych...",
"Deleting pipeline...": "Usuwanie strumienia...",
"Delimiter": "Separator",
"Delta font size": "Rozmiar czcionki delty",
"Depending on number and size of exported files, this might take a
while...": "W zależności od liczby i rozmiaru eksportowanych plików może to
chwilę potrwać...",
"Description": "Opis",
- "Deselect All": "Odznacz wszystko",
"Deselect all": "Odznacz wszystko",
"Details": "Szczegóły",
"Dialog Title": "Tytuł okna dialogowego",
@@ -773,9 +772,6 @@
"Order": "Kolejność",
"Original": "Oryginalne",
"Original (Parsed)": "Oryginał (przetworzony)",
- "Orphaned": "Osierocone",
- "Orphaned adapter instance": "Osierocona instancja adaptera",
- "Orphaned pipeline element instance": "Osierocona instancja elementu
strumienia",
"Others Label": "Etykieta dla pozostałych",
"Output Topics": "Tematy wyjściowe",
"Output stream": "Strumień wyjściowy",
@@ -863,6 +859,7 @@
"Refresh charts": "Odśwież wykresy",
"Refresh interval": "Interwał odświeżania",
"Refresh metrics": "Odśwież metryki",
+ "Refresh running instances": "Odśwież uruchomione instancje",
"Region": "Region",
"Register": "Zarejestruj się",
"Registered services": "Zarejestrowane usługi",
@@ -956,7 +953,6 @@
"Second": "Sekunda",
"Secret Key": "Klucz tajny",
"Select Adapter": "Wybierz adapter",
- "Select All": "Zaznacz wszystko",
"Select CSV file": "Wybierz plik CSV",
"Select Data": "Wybierz dane",
"Select Fields": "Wybierz pola",
diff --git
a/ui/src/app/configuration/dialog/extensions-service-details/extensions-service-details-dialog.component.html
b/ui/src/app/configuration/dialog/extensions-service-details/extensions-service-details-dialog.component.html
index 8122b4f195..007a1c8414 100644
---
a/ui/src/app/configuration/dialog/extensions-service-details/extensions-service-details-dialog.component.html
+++
b/ui/src/app/configuration/dialog/extensions-service-details/extensions-service-details-dialog.component.html
@@ -55,7 +55,7 @@
</div>
</div>
<mat-divider></mat-divider>
- <div class="sp-dialog-actions actions-align-right">
+ <div class="sp-dialog-actions">
<button mat-button mat-flat-button class="mat-basic" (click)="close()">
{{ 'Close' | translate }}
</button>
diff --git
a/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.html
b/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.html
new file mode 100644
index 0000000000..3253a9b4e9
--- /dev/null
+++
b/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.html
@@ -0,0 +1,454 @@
+<!--
+~ 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">
+ <div fxLayout="column" fxLayoutGap="20px" class="p-15">
+ <div fxLayout="row" fxLayoutAlign="space-between start">
+ <div fxLayout="column" fxLayoutGap="5px">
+ <div class="text-md">
+ {{ serviceReg.svcId }}
+ </div>
+ <div class="text-sm">
+ {{ serviceReg.svcGroup }}
+ </div>
+ </div>
+ <div>
+ <button
+ mat-icon-button
+ color="accent"
+ [disabled]="loading || removing"
+ [matTooltip]="'Refresh running instances' | translate"
+ (click)="loadRunningInstances()"
+ >
+ <mat-icon>refresh</mat-icon>
+ </button>
+ </div>
+ </div>
+
+ @if (loading) {
+ <div
+ fxLayout="row"
+ fxLayoutAlign="center center"
+ class="loading-container"
+ >
+ <mat-spinner diameter="32"></mat-spinner>
+ </div>
+ }
+
+ @if (!loading && loadError) {
+ <div class="empty-state">
+ {{ 'Could not load running instances' | translate }}
+ </div>
+ }
+
+ @if (!loading && !loadError) {
+ <div fxLayout="column" fxLayoutGap="10px">
+ <div class="section-title">
+ {{ 'Adapters' | translate }}
+ </div>
+ @if (adapterDataSource.data.length > 0) {
+ <sp-table
+ fxFlex="100"
+ [dataSource]="adapterDataSource"
+ [columns]="adapterColumns"
+ [nameSearchConfig]="adapterNameSearchConfig"
+ >
+ <ng-container matColumnDef="name">
+ <th
+ fxFlex="25"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Name' | translate }}
+ </th>
+ <td
+ fxFlex="25"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let adapter"
+ >
+ {{ adapter.displayName | translate }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="state">
+ <th
+ fxFlex="15"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'State' | translate }}
+ </th>
+ <td
+ fxFlex="15"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let adapter"
+ >
+ <sp-label
+ size="small"
+ variant="soft"
+ [tone]="
+ getAdapterStateTone(adapter.state)
+ "
+ [labelText]="adapter.state | translate"
+ >
+ </sp-label>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="relation">
+ <th
+ fxFlex="15"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Relation' | translate }}
+ </th>
+ <td
+ fxFlex="15"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let adapter"
+ >
+ <sp-label
+ size="small"
+ variant="soft"
+ [tone]="
+ getRelationTone(adapter.orphaned)
+ "
+ [labelText]="
+ adapter.relation | translate
+ "
+ >
+ </sp-label>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="instanceId">
+ <th
+ fxFlex="35"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Instance ID' | translate }}
+ </th>
+ <td
+ fxFlex="35"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let adapter"
+ class="instance-id"
+ >
+ {{ adapter.instanceId }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="action">
+ <th
+ fxFlex="10"
+ fxLayoutAlign="end center"
+ mat-header-cell
+ *matHeaderCellDef
+ ></th>
+ <td
+ fxFlex="10"
+ fxLayoutAlign="end center"
+ mat-cell
+ *matCellDef="let adapter"
+ >
+ <button
+ mat-icon-button
+ class="btn-warn"
+ [disabled]="removing"
+ [matTooltip]="
+ 'Remove instance from service'
+ | translate
+ "
+ (click)="
+ requestRemoveAdapterInstance(
+ adapter
+ );
+ $event.stopPropagation()
+ "
+ >
+ <mat-icon>delete</mat-icon>
+ </button>
+ </td>
+ </ng-container>
+ </sp-table>
+ } @else {
+ <div class="empty-state">
+ {{ 'No running adapters' | translate }}
+ </div>
+ }
+ </div>
+
+ <div fxLayout="column" fxLayoutGap="10px">
+ <div class="section-title">
+ {{ 'Pipeline elements' | translate }}
+ </div>
+ @if (pipelineElementDataSource.data.length > 0) {
+ <sp-table
+ fxFlex="100"
+ [dataSource]="pipelineElementDataSource"
+ [columns]="pipelineElementColumns"
+
[nameSearchConfig]="pipelineElementNameSearchConfig"
+ >
+ <ng-container matColumnDef="name">
+ <th
+ fxFlex="20"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Name' | translate }}
+ </th>
+ <td
+ fxFlex="20"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let element"
+ >
+ {{ element.displayName | translate }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="pipelineName">
+ <th
+ fxFlex="20"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Pipeline' | translate }}
+ </th>
+ <td
+ fxFlex="20"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let element"
+ >
+ {{ element.pipelineDisplayName }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="type">
+ <th
+ fxFlex="12"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Type' | translate }}
+ </th>
+ <td
+ fxFlex="12"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let element"
+ >
+ <sp-label
+ size="small"
+ variant="soft"
+ [tone]="
+ getPipelineElementTypeTone(
+ element.type
+ )
+ "
+ [labelText]="element.type | translate"
+ >
+ </sp-label>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="relation">
+ <th
+ fxFlex="13"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Relation' | translate }}
+ </th>
+ <td
+ fxFlex="13"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let element"
+ >
+ <sp-label
+ size="small"
+ variant="soft"
+ [tone]="
+ getRelationTone(element.orphaned)
+ "
+ [labelText]="
+ element.relation | translate
+ "
+ >
+ </sp-label>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="instanceId">
+ <th
+ fxFlex="25"
+ fxLayoutAlign="start center"
+ mat-header-cell
+ *matHeaderCellDef
+ >
+ {{ 'Instance ID' | translate }}
+ </th>
+ <td
+ fxFlex="25"
+ fxLayoutAlign="start center"
+ mat-cell
+ *matCellDef="let element"
+ class="instance-id"
+ >
+ {{ element.instanceId }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="action">
+ <th
+ fxFlex="10"
+ fxLayoutAlign="end center"
+ mat-header-cell
+ *matHeaderCellDef
+ ></th>
+ <td
+ fxFlex="10"
+ fxLayoutAlign="end center"
+ mat-cell
+ *matCellDef="let element"
+ >
+ <button
+ mat-icon-button
+ color="accent"
+ [disabled]="removing"
+ [matTooltip]="
+ 'Remove instance from service'
+ | translate
+ "
+ (click)="
+
requestRemovePipelineElementInstance(
+ element
+ );
+ $event.stopPropagation()
+ "
+ >
+ <mat-icon>delete</mat-icon>
+ </button>
+ </td>
+ </ng-container>
+ </sp-table>
+ } @else {
+ <div class="empty-state">
+ {{ 'No running pipeline elements' | translate }}
+ </div>
+ }
+ </div>
+ }
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div
+ class="sp-dialog-actions running-instances-actions"
+ fxLayout="column"
+ fxLayoutGap="10px"
+ >
+ @if (pendingRemoval) {
+ <sp-alert-banner
+ class="confirmation-alert"
+ type="warning"
+ icon="warning"
+ [title]="getPendingRemovalTitle() | translate"
+ [description]="
+ 'Deleted instances of active pipelines or adapters will be
redeployed by the health check.'
+ | translate
+ "
+ >
+ <div
+ class="confirmation-content"
+ fxLayout="column"
+ fxLayoutAlign="start start"
+ fxLayoutGap="10px"
+ >
+ <div class="text-sm instance-id">
+ {{ getPendingRemovalDescription() | translate }}
+ </div>
+ <div
+ fxLayout="row"
+ fxLayoutAlign="start center"
+ fxLayoutGap="10px"
+ >
+ <button
+ mat-button
+ mat-flat-button
+ class="mat-basic"
+ [disabled]="removing"
+ (click)="cancelRemoval()"
+ >
+ {{ 'Cancel' | translate }}
+ </button>
+ <button
+ mat-button
+ mat-flat-button
+ color="accent"
+ [disabled]="removing"
+ (click)="confirmRemoval()"
+ >
+ {{ 'Remove' | translate }}
+ </button>
+ </div>
+ </div>
+ </sp-alert-banner>
+ }
+ <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px">
+ <button
+ mat-button
+ mat-flat-button
+ class="btn-warn"
+ [disabled]="
+ removing ||
+ loading ||
+ totalInstanceCount === 0 ||
+ !!pendingRemoval
+ "
+ (click)="requestRemoveAllInstances()"
+ >
+ {{ 'Remove all instances' | translate }}
+ </button>
+ <button
+ mat-button
+ mat-flat-button
+ class="mat-basic"
+ (click)="close()"
+ >
+ {{ 'Close' | translate }}
+ </button>
+ </div>
+ </div>
+</div>
diff --git
a/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.scss
b/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.scss
new file mode 100644
index 0000000000..18d4777d19
--- /dev/null
+++
b/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.scss
@@ -0,0 +1,48 @@
+/*!
+ * 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.
+ *
+ */
+
+.loading-container {
+ min-height: 120px;
+}
+
+.section-title {
+ font-weight: 600;
+}
+
+.empty-state {
+ color: var(--color-secondary-text);
+ padding: 10px 0;
+}
+
+.confirmation-content {
+ margin-top: 8px;
+}
+
+.confirmation-alert {
+ display: block;
+ width: 100%;
+}
+
+.running-instances-actions {
+ align-items: stretch;
+ justify-content: flex-start;
+}
+
+.instance-id {
+ overflow-wrap: anywhere;
+}
diff --git
a/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.ts
b/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.ts
new file mode 100644
index 0000000000..02dcb597ae
--- /dev/null
+++
b/ui/src/app/configuration/dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component.ts
@@ -0,0 +1,375 @@
+/*
+ * 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, Input, OnInit, inject } from '@angular/core';
+import { MatButton, MatIconButton } from '@angular/material/button';
+import { MatDivider } from '@angular/material/divider';
+import { MatIcon } from '@angular/material/icon';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import {
+ MatCell,
+ MatCellDef,
+ MatColumnDef,
+ MatHeaderCell,
+ MatHeaderCellDef,
+ MatTableDataSource,
+} from '@angular/material/table';
+import { MatTooltip } from '@angular/material/tooltip';
+import { TranslatePipe } from '@ngx-translate/core';
+import { Observable } from 'rxjs';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ LayoutGapDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { SpServiceRegistration } from '@streampipes/platform-services';
+import {
+ DialogRef,
+ SpAlertBannerComponent,
+ SpLabelComponent,
+ SpTableComponent,
+ SpTableNameSearchConfig,
+} from '@streampipes/shared-ui';
+import {
+ ConfigurationService,
+ RunningAdapterInstance,
+ RunningExtensionInstances,
+ RunningPipelineElementInstance,
+} from '../../shared/configuration.service';
+
+type RunningAdapterInstanceRow = RunningAdapterInstance & {
+ displayName: string;
+ relation: string;
+};
+
+type RunningPipelineElementInstanceRow = RunningPipelineElementInstance & {
+ displayName: string;
+ pipelineDisplayName: string;
+ relation: string;
+};
+
+type PendingRemoval =
+ | {
+ type: 'adapter';
+ item: RunningAdapterInstanceRow;
+ }
+ | {
+ type: 'pipelineElement';
+ item: RunningPipelineElementInstanceRow;
+ }
+ | {
+ type: 'all';
+ };
+
+@Component({
+ selector: 'sp-extensions-service-running-instances-dialog',
+ templateUrl:
'./extensions-service-running-instances-dialog.component.html',
+ styleUrls:
['./extensions-service-running-instances-dialog.component.scss'],
+ imports: [
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ LayoutGapDirective,
+ MatButton,
+ MatCell,
+ MatCellDef,
+ MatColumnDef,
+ MatDivider,
+ MatHeaderCell,
+ MatHeaderCellDef,
+ MatIcon,
+ MatIconButton,
+ MatProgressSpinner,
+ MatTooltip,
+ SpAlertBannerComponent,
+ SpLabelComponent,
+ SpTableComponent,
+ TranslatePipe,
+ ],
+})
+export class SpExtensionsServiceRunningInstancesDialogComponent implements
OnInit {
+ private dialogRef =
+ inject<DialogRef<SpExtensionsServiceRunningInstancesDialogComponent>>(
+ DialogRef,
+ );
+ private configurationService = inject(ConfigurationService);
+
+ @Input()
+ serviceReg: SpServiceRegistration;
+
+ adapterColumns = ['name', 'state', 'relation', 'instanceId', 'action'];
+ pipelineElementColumns = [
+ 'name',
+ 'pipelineName',
+ 'type',
+ 'relation',
+ 'instanceId',
+ 'action',
+ ];
+ runningInstances: RunningExtensionInstances;
+ adapterDataSource = new MatTableDataSource<RunningAdapterInstanceRow>();
+ pipelineElementDataSource =
+ new MatTableDataSource<RunningPipelineElementInstanceRow>();
+ adapterNameSearchConfig:
SpTableNameSearchConfig<RunningAdapterInstanceRow> =
+ {
+ enabled: true,
+ placeholder: 'Filter adapters',
+ searchKey: [
+ 'displayName',
+ 'instanceId',
+ 'appId',
+ 'state',
+ 'relation',
+ ],
+ };
+ pipelineElementNameSearchConfig:
SpTableNameSearchConfig<RunningPipelineElementInstanceRow> =
+ {
+ enabled: true,
+ placeholder: 'Filter pipeline elements',
+ searchKey: [
+ 'displayName',
+ 'pipelineDisplayName',
+ 'instanceId',
+ 'appId',
+ 'pipelineId',
+ 'type',
+ 'relation',
+ ],
+ };
+ loading = true;
+ loadError = false;
+ removing = false;
+ pendingRemoval?: PendingRemoval;
+
+ get totalInstanceCount(): number {
+ return (
+ this.adapterDataSource.data.length +
+ this.pipelineElementDataSource.data.length
+ );
+ }
+
+ ngOnInit(): void {
+ this.loadRunningInstances();
+ }
+
+ loadRunningInstances(): void {
+ this.loading = true;
+ this.loadError = false;
+ this.configurationService
+ .getRunningExtensionInstances(this.serviceReg.svcId)
+ .subscribe({
+ next: response => {
+ this.runningInstances = response;
+ this.adapterDataSource.data = response.adapters.map(
+ adapter => this.makeAdapterRow(adapter),
+ );
+ this.pipelineElementDataSource.data =
+ response.pipelineElements.map(pipelineElement =>
+ this.makePipelineElementRow(pipelineElement),
+ );
+ this.loading = false;
+ },
+ error: () => {
+ this.loading = false;
+ this.loadError = true;
+ },
+ });
+ }
+
+ requestRemoveAdapterInstance(adapter: RunningAdapterInstanceRow): void {
+ this.pendingRemoval = {
+ type: 'adapter',
+ item: adapter,
+ };
+ }
+
+ requestRemovePipelineElementInstance(
+ pipelineElement: RunningPipelineElementInstanceRow,
+ ): void {
+ this.pendingRemoval = {
+ type: 'pipelineElement',
+ item: pipelineElement,
+ };
+ }
+
+ requestRemoveAllInstances(): void {
+ this.pendingRemoval = {
+ type: 'all',
+ };
+ }
+
+ cancelRemoval(): void {
+ this.pendingRemoval = undefined;
+ }
+
+ confirmRemoval(): void {
+ const pendingRemoval = this.pendingRemoval;
+
+ if (!pendingRemoval) {
+ return;
+ }
+
+ this.pendingRemoval = undefined;
+
+ if (pendingRemoval.type === 'adapter') {
+ this.removeAdapterInstance(pendingRemoval.item);
+ } else if (pendingRemoval.type === 'pipelineElement') {
+ this.removePipelineElementInstance(pendingRemoval.item);
+ } else {
+ this.removeAllInstances();
+ }
+ }
+
+ getPendingRemovalTitle(): string {
+ if (!this.pendingRemoval) {
+ return '';
+ }
+
+ if (this.pendingRemoval.type === 'adapter') {
+ return 'Remove adapter instance?';
+ } else if (this.pendingRemoval.type === 'pipelineElement') {
+ return 'Remove pipeline element instance?';
+ } else {
+ return 'Remove all running instances?';
+ }
+ }
+
+ getPendingRemovalDescription(): string {
+ if (!this.pendingRemoval) {
+ return '';
+ }
+
+ if (this.pendingRemoval.type === 'adapter') {
+ return this.pendingRemoval.item.instanceId;
+ } else if (this.pendingRemoval.type === 'pipelineElement') {
+ return this.pendingRemoval.item.instanceId;
+ } else {
+ return 'This removes all instances currently reported by this
extension service.';
+ }
+ }
+
+ private removeAdapterInstance(adapter: RunningAdapterInstance): void {
+ this.removeInstance(() =>
+ this.configurationService.removeRunningAdapterInstance(
+ this.serviceReg.svcId,
+ adapter.instanceId,
+ ),
+ );
+ }
+
+ private removePipelineElementInstance(
+ pipelineElement: RunningPipelineElementInstance,
+ ): void {
+ this.removeInstance(() =>
+ this.configurationService.removeRunningPipelineElementInstance(
+ this.serviceReg.svcId,
+ pipelineElement.instanceId,
+ ),
+ );
+ }
+
+ private removeAllInstances(): void {
+ this.removeInstance(() =>
+ this.configurationService.removeAllRunningExtensionInstances(
+ this.serviceReg.svcId,
+ ),
+ );
+ }
+
+ close(): void {
+ this.dialogRef.close();
+ }
+
+ getAdapterStateTone(
+ state: RunningAdapterInstance['state'],
+ ): 'success' | 'warning' | 'info' | 'neutral' {
+ if (state === 'RUNNING') {
+ return 'success';
+ } else if (state === 'STOPPING') {
+ return 'warning';
+ } else if (state === 'STARTING') {
+ return 'info';
+ } else {
+ return 'neutral';
+ }
+ }
+
+ getPipelineElementTypeTone(
+ type: RunningPipelineElementInstance['type'],
+ ): 'info' | 'neutral' | 'warning' {
+ if (type === 'PROCESSOR') {
+ return 'info';
+ } else if (type === 'SINK') {
+ return 'neutral';
+ } else {
+ return 'warning';
+ }
+ }
+
+ getRelationLabel(orphaned: boolean): string {
+ return orphaned ? 'Orphaned' : 'Configured';
+ }
+
+ getRelationTone(orphaned: boolean): 'success' | 'warning' {
+ return orphaned ? 'warning' : 'success';
+ }
+
+ private makeAdapterRow(
+ adapter: RunningAdapterInstance,
+ ): RunningAdapterInstanceRow {
+ return {
+ ...adapter,
+ displayName: adapter.orphaned
+ ? 'Orphaned adapter instance'
+ : (adapter.name ?? ''),
+ relation: this.getRelationLabel(adapter.orphaned),
+ };
+ }
+
+ private makePipelineElementRow(
+ pipelineElement: RunningPipelineElementInstance,
+ ): RunningPipelineElementInstanceRow {
+ return {
+ ...pipelineElement,
+ displayName: pipelineElement.orphaned
+ ? 'Orphaned pipeline element instance'
+ : (pipelineElement.name ?? ''),
+ pipelineDisplayName: pipelineElement.orphaned
+ ? '-'
+ : (pipelineElement.pipelineName ?? ''),
+ relation: this.getRelationLabel(pipelineElement.orphaned),
+ };
+ }
+
+ private removeInstance(removeFn: () => Observable<object>): void {
+ this.removing = true;
+ this.loadError = false;
+ removeFn().subscribe({
+ next: () => {
+ this.removing = false;
+ this.pendingRemoval = undefined;
+ this.loadRunningInstances();
+ },
+ error: () => {
+ this.removing = false;
+ this.loadError = true;
+ },
+ });
+ }
+}
diff --git
a/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
b/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
index 61c79ce624..b4dc934aa2 100644
---
a/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
+++
b/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.html
@@ -53,7 +53,7 @@
<ng-container matColumnDef="group">
<th
- fxFlex="45"
+ fxFlex="40"
fxLayoutAlign="start center"
mat-header-cell
*matHeaderCellDef
@@ -61,7 +61,7 @@
{{ 'Service Group' | translate }}
</th>
<td
- fxFlex="45"
+ fxFlex="40"
fxLayoutAlign="start center"
mat-cell
*matCellDef="let element"
@@ -91,7 +91,7 @@
<ng-container matColumnDef="action">
<th
- fxFlex="10"
+ fxFlex="15"
fxLayoutAlign="end center"
mat-header-cell
*matHeaderCellDef
@@ -106,11 +106,19 @@
</button>
</th>
<td
- fxFlex="10"
+ fxFlex="15"
fxLayoutAlign="end center"
mat-cell
*matCellDef="let element"
>
+ <button
+ mat-icon-button
+ color="accent"
+ [matTooltip]="'View running instances' | translate"
+ (click)="openRunningInstances(element)"
+ >
+ <mat-icon>list_alt</mat-icon>
+ </button>
<button
mat-icon-button
color="accent"
diff --git
a/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.ts
b/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.ts
index 9492e12313..785c15d1d7 100644
---
a/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.ts
+++
b/ui/src/app/configuration/extensions-service-management/registered-extensions-services/registered-extensions-services.component.ts
@@ -35,6 +35,7 @@ import { SpServiceRegistration } from
'@streampipes/platform-services';
import { ConfigurationService } from '../../shared/configuration.service';
import { DialogService, PanelType } from '@streampipes/shared-ui';
import { SpExtensionsServiceDetailsDialogComponent } from
'../../dialog/extensions-service-details/extensions-service-details-dialog.component';
+import { SpExtensionsServiceRunningInstancesDialogComponent } from
'../../dialog/extensions-service-running-instances/extensions-service-running-instances-dialog.component';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import {
FlexDirective,
@@ -92,7 +93,7 @@ export class SpRegisteredExtensionsServiceComponent {
openServiceDetails(serviceReg: SpServiceRegistration) {
this.dialogService.open(SpExtensionsServiceDetailsDialogComponent, {
- panelType: PanelType.STANDARD_PANEL,
+ panelType: PanelType.SLIDE_IN_PANEL,
title: this.translateService.instant('Service details'),
width: '70vw',
data: {
@@ -100,4 +101,18 @@ export class SpRegisteredExtensionsServiceComponent {
},
});
}
+
+ openRunningInstances(serviceReg: SpServiceRegistration) {
+ this.dialogService.open(
+ SpExtensionsServiceRunningInstancesDialogComponent,
+ {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: this.translateService.instant('Running instances'),
+ width: '80vw',
+ data: {
+ serviceReg,
+ },
+ },
+ );
+ }
}
diff --git a/ui/src/app/configuration/shared/configuration.service.ts
b/ui/src/app/configuration/shared/configuration.service.ts
index 474d5d8a30..8bee9123c2 100644
--- a/ui/src/app/configuration/shared/configuration.service.ts
+++ b/ui/src/app/configuration/shared/configuration.service.ts
@@ -27,6 +27,30 @@ import {
SpServiceRegistration,
} from '@streampipes/platform-services';
+export interface RunningExtensionInstances {
+ serviceId: string;
+ adapters: RunningAdapterInstance[];
+ pipelineElements: RunningPipelineElementInstance[];
+}
+
+export interface RunningAdapterInstance {
+ instanceId: string;
+ name?: string;
+ appId?: string;
+ state: 'RUNNING' | 'STARTING' | 'STOPPING';
+ orphaned: boolean;
+}
+
+export interface RunningPipelineElementInstance {
+ instanceId: string;
+ name?: string;
+ appId?: string;
+ pipelineId?: string;
+ pipelineName?: string;
+ type: 'PROCESSOR' | 'SINK' | 'UNKNOWN';
+ orphaned: boolean;
+}
+
@Injectable({ providedIn: 'root' })
export class ConfigurationService {
private http = inject(HttpClient);
@@ -68,6 +92,48 @@ export class ConfigurationService {
);
}
+ getRunningExtensionInstances(
+ serviceId: string,
+ ): Observable<RunningExtensionInstances> {
+ return this.http
+ .get(
+ this.getServerUrl() +
+
`/api/v2/extensions-services/${encodeURIComponent(serviceId)}/running-instances`,
+ )
+ .pipe(
+ map(response => {
+ return response as RunningExtensionInstances;
+ }),
+ );
+ }
+
+ removeAllRunningExtensionInstances(serviceId: string): Observable<object> {
+ return this.http.delete(
+ this.getServerUrl() +
+
`/api/v2/extensions-services/${encodeURIComponent(serviceId)}/running-instances`,
+ );
+ }
+
+ removeRunningAdapterInstance(
+ serviceId: string,
+ instanceId: string,
+ ): Observable<object> {
+ return this.http.delete(
+ this.getServerUrl() +
+
`/api/v2/extensions-services/${encodeURIComponent(serviceId)}/running-instances/adapters/${encodeURIComponent(instanceId)}`,
+ );
+ }
+
+ removeRunningPipelineElementInstance(
+ serviceId: string,
+ instanceId: string,
+ ): Observable<object> {
+ return this.http.delete(
+ this.getServerUrl() +
+
`/api/v2/extensions-services/${encodeURIComponent(serviceId)}/running-instances/pipeline-elements/${encodeURIComponent(instanceId)}`,
+ );
+ }
+
getExtensionsServiceConfigs(): Observable<SpServiceConfiguration[]> {
return this.http
.get(