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

zehnder pushed a commit to branch test-uns
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit d44020160a9cdf7c304dbffcf0614b3b47f0e18c
Author: Philipp Zehnder <[email protected]>
AuthorDate: Fri Mar 20 08:46:14 2026 +0100

    feat: First prototype
---
 .../management/AdapterMasterManagement.java        |   5 +-
 .../connect/management/util/GroundingUtils.java    |  10 +-
 .../model/connect/adapter/AdapterDescription.java  |  11 +
 .../src/lib/model/gen/streampipes-model.ts         |   2 +
 .../start-adapter-configuration.component.html     |  19 +
 .../start-adapter-configuration.component.ts       |   9 +-
 .../existing-adapters.component.html               | 525 +++++++++++++--------
 .../existing-adapters.component.scss               |  30 ++
 .../existing-adapters.component.ts                 | 110 +++++
 9 files changed, 520 insertions(+), 201 deletions(-)

diff --git 
a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
 
b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
index 925f2447ef..86e20a2c12 100644
--- 
a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
+++ 
b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java
@@ -75,8 +75,11 @@ public class AdapterMasterManagement {
     
adapterDescription.setCorrespondingDataStreamElementId(dataStreamElementId);
 
     // Add EventGrounding to AdapterDescription
-    var eventGrounding = GroundingUtils.createEventGrounding();
+    var eventGrounding = 
GroundingUtils.createEventGrounding(adapterDescription.getTopicName());
     adapterDescription.setEventGrounding(eventGrounding);
+    adapterDescription.setTopicName(
+        
eventGrounding.getTransportProtocol().getTopicDefinition().getActualTopicName()
+    );
 
     this.adapterResourceManager.encryptAndCreate(adapterDescription);
 
diff --git 
a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/util/GroundingUtils.java
 
b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/util/GroundingUtils.java
index 92870f490c..f9bfbfb8b1 100644
--- 
a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/util/GroundingUtils.java
+++ 
b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/util/GroundingUtils.java
@@ -29,6 +29,8 @@ import 
org.apache.streampipes.model.grounding.SimpleTopicDefinition;
 import org.apache.streampipes.model.grounding.TopicDefinition;
 import org.apache.streampipes.model.grounding.TransportProtocol;
 
+import org.apache.commons.lang3.StringUtils;
+
 import java.util.UUID;
 
 public class GroundingUtils {
@@ -36,13 +38,19 @@ public class GroundingUtils {
   private static final String TOPIC_PREFIX = "org.apache.streampipes.connect.";
 
   public static EventGrounding createEventGrounding() {
+    return createEventGrounding(null);
+  }
+
+  public static EventGrounding createEventGrounding(String requestedTopicName) 
{
     EventGrounding eventGrounding = new EventGrounding();
     var messagingSettings = Utils
         .getCoreConfigStorage()
         .get()
         .getMessagingSettings();
 
-    String topic = TOPIC_PREFIX + UUID.randomUUID().toString();
+    String topic = StringUtils.isBlank(requestedTopicName)
+        ? TOPIC_PREFIX + UUID.randomUUID()
+        : requestedTopicName.trim();
     TopicDefinition topicDefinition = new SimpleTopicDefinition(topic);
 
     SpProtocol prioritizedProtocol =
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/AdapterDescription.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/AdapterDescription.java
index d0579f6c78..eefa31630f 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/AdapterDescription.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/adapter/AdapterDescription.java
@@ -66,6 +66,8 @@ public class AdapterDescription extends 
VersionedNamedStreamPipesEntity {
 
   private TransformationConfig transformationConfig;
 
+  private String topicName;
+
   public AdapterDescription() {
     super();
     this.rules = new ArrayList<>();
@@ -105,6 +107,7 @@ public class AdapterDescription extends 
VersionedNamedStreamPipesEntity {
     this.running = other.isRunning();
     this.deploymentConfiguration = other.getDeploymentConfiguration();
     this.transformationConfig = other.getTransformationConfig();
+    this.topicName = other.getTopicName();
   }
 
   public String getRev() {
@@ -233,4 +236,12 @@ public class AdapterDescription extends 
VersionedNamedStreamPipesEntity {
   public void setTransformationConfig(TransformationConfig 
transformationConfig) {
     this.transformationConfig = transformationConfig;
   }
+
+  public String getTopicName() {
+    return topicName;
+  }
+
+  public void setTopicName(String topicName) {
+    this.topicName = topicName;
+  }
 }
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 a0fced120e..615bad00ee 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
@@ -114,6 +114,7 @@ export class AdapterDescription extends 
VersionedNamedStreamPipesEntity {
     'rules': TransformationRuleDescriptionUnion[];
     'running': boolean;
     'selectedEndpointUrl': string;
+    'topicName': string;
     'transformationConfig': TransformationConfig;
 
     static 'fromData'(
@@ -145,6 +146,7 @@ export class AdapterDescription extends 
VersionedNamedStreamPipesEntity {
         )(data.rules);
         instance.running = data.running;
         instance.selectedEndpointUrl = data.selectedEndpointUrl;
+        instance.topicName = data.topicName;
         instance.transformationConfig = TransformationConfig.fromData(
             data.transformationConfig,
         );
diff --git 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
index f12efdc35e..c988134b88 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.html
@@ -90,6 +90,25 @@
                         />
                     </mat-form-field>
                 </sp-form-field>
+                @if (!isEditMode) {
+                    <sp-form-field
+                        [level]="2"
+                        [label]="'Topic Name (Optional)' | translate"
+                    >
+                        <mat-form-field color="accent">
+                            <input
+                                formControlName="adapterTopicName"
+                                matInput
+                                id="input-AdapterTopicName"
+                                [placeholder]="
+                                    'Leave empty to auto-generate a topic'
+                                        | translate
+                                "
+                                data-cy="sp-adapter-topic-name"
+                            />
+                        </mat-form-field>
+                    </sp-form-field>
+                }
             </div>
         </div>
     </sp-basic-inner-panel>
diff --git 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
index d0219bd64b..1688e75d94 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/start-adapter-configuration/start-adapter-configuration.component.ts
@@ -172,9 +172,14 @@ export class StartAdapterConfigurationComponent implements 
OnInit {
                 ValidateName(),
             ]),
         );
-        this.startAdapterForm.valueChanges.subscribe(
-            v => (this.adapterDescription.name = v.adapterName),
+        this.startAdapterForm.addControl(
+            'adapterTopicName',
+            new UntypedFormControl(this.adapterDescription.topicName ?? ''),
         );
+        this.startAdapterForm.valueChanges.subscribe(v => {
+            this.adapterDescription.name = v.adapterName;
+            this.adapterDescription.topicName = v.adapterTopicName?.trim();
+        });
         this.startAdapterForm.statusChanges.subscribe(() => {
             this.startAdapterSettingsFormValid = this.startAdapterForm.valid;
         });
diff --git 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
index 91ba5b3cf1..7af1986838 100644
--- 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
+++ 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html
@@ -40,6 +40,18 @@
                 (filterChangedEmitter)="applyFilter($event)"
             >
             </sp-connect-filter-toolbar>
+            <mat-button-toggle-group
+                class="overview-mode-toggle"
+                [value]="overviewMode"
+                (change)="setOverviewMode($event.value)"
+            >
+                <mat-button-toggle value="table">
+                    {{ 'Table' | translate }}
+                </mat-button-toggle>
+                <mat-button-toggle value="uns">
+                    {{ 'UNS' | translate }}
+                </mat-button-toggle>
+            </mat-button-toggle-group>
             <div
                 fxFlex="100"
                 fxLayout="row"
@@ -72,209 +84,328 @@
         <sp-basic-header-title-component
             [title]="'Adapters' | translate"
         ></sp-basic-header-title-component>
-        <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
-            <sp-table
-                fxFlex="100"
-                featureCardId="adapter"
-                resourceIdKey="elementId"
-                [columns]="displayedColumns"
-                [dataSource]="dataSource"
-                [assetContextConfig]="assetContextConfig"
-                [showSelectionCheckboxes]="true"
-                [showMultiActionsExecuteButton]="true"
-                [multiActionOptions]="bulkAdapterActionOptions"
-                [showActionsMenu]="true"
-                [rowsClickable]="true"
-                (multiActionsExecute)="startStopSelectedAdapters($event)"
-                (rowClicked)="navigateToDetailsOverviewPage($event)"
-                data-cy="all-adapters-table"
-                matSort
-            >
-                <ng-container matColumnDef="status">
-                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                        {{ 'Status' | translate }}
-                    </th>
-                    <td mat-cell *matCellDef="let adapter">
-                        <sp-adapter-status-light
-                            [adapterRunning]="adapter.running"
-                        ></sp-adapter-status-light>
-                    </td>
-                </ng-container>
-                <ng-container matColumnDef="start">
-                    <th mat-header-cell *matHeaderCellDef>Start</th>
-                    <td
-                        (click)="$event.stopPropagation()"
-                        mat-cell
-                        *matCellDef="let adapter"
-                        data-cy="adapters-table"
-                    >
-                        @if (
-                            adapter.elementId === operationInProgressAdapterId
-                        ) {
+        @if (overviewMode === 'table') {
+            <div fxFlex="100" fxLayout="row" fxLayoutAlign="center start">
+                <sp-table
+                    fxFlex="100"
+                    featureCardId="adapter"
+                    resourceIdKey="elementId"
+                    [columns]="displayedColumns"
+                    [dataSource]="dataSource"
+                    [assetContextConfig]="assetContextConfig"
+                    [showSelectionCheckboxes]="true"
+                    [showMultiActionsExecuteButton]="true"
+                    [multiActionOptions]="bulkAdapterActionOptions"
+                    [showActionsMenu]="true"
+                    [rowsClickable]="true"
+                    (multiActionsExecute)="startStopSelectedAdapters($event)"
+                    (rowClicked)="navigateToDetailsOverviewPage($event)"
+                    data-cy="all-adapters-table"
+                    matSort
+                >
+                    <ng-container matColumnDef="status">
+                        <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                            {{ 'Status' | translate }}
+                        </th>
+                        <td mat-cell *matCellDef="let adapter">
+                            <sp-adapter-status-light
+                                [adapterRunning]="adapter.running"
+                            ></sp-adapter-status-light>
+                        </td>
+                    </ng-container>
+                    <ng-container matColumnDef="start">
+                        <th mat-header-cell *matHeaderCellDef>Start</th>
+                        <td
+                            (click)="$event.stopPropagation()"
+                            mat-cell
+                            *matCellDef="let adapter"
+                            data-cy="adapters-table"
+                        >
+                            @if (
+                                adapter.elementId ===
+                                operationInProgressAdapterId
+                            ) {
+                                <div
+                                    
data-cy="adapter-operation-in-progress-spinner"
+                                    fxLayoutAlign="center center"
+                                >
+                                    <mat-spinner
+                                        color="accent"
+                                        [diameter]="20"
+                                    ></mat-spinner>
+                                </div>
+                            } @else if (!adapter.running) {
+                                <button
+                                    color="accent"
+                                    mat-icon-button
+                                    matTooltip="Start adapter"
+                                    matTooltipPosition="above"
+                                    data-cy="start-adapter"
+                                    (click)="
+                                        startAdapter(adapter);
+                                        $event.stopPropagation()
+                                    "
+                                >
+                                    <i class="material-icons">play_arrow</i>
+                                </button>
+                            } @else if (adapter.running) {
+                                <button
+                                    color="accent"
+                                    mat-icon-button
+                                    matTooltip="Stop adapter"
+                                    matTooltipPosition="above"
+                                    data-cy="stop-adapter"
+                                    (click)="
+                                        stopAdapter(adapter);
+                                        $event.stopPropagation()
+                                    "
+                                >
+                                    <i class="material-icons">stop</i>
+                                </button>
+                            }
+                        </td>
+                    </ng-container>
+
+                    <ng-container matColumnDef="name">
+                        <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                            {{ 'Name' | translate }}
+                        </th>
+                        <td mat-cell *matCellDef="let adapter">
                             <div
-                                data-cy="adapter-operation-in-progress-spinner"
-                                fxLayoutAlign="center center"
+                                fxLayout="column"
+                                fxLayoutAlign="start start"
+                                class="truncate"
                             >
-                                <mat-spinner
-                                    color="accent"
-                                    [diameter]="20"
-                                ></mat-spinner>
+                                <span data-cy="adapter-name">{{
+                                    adapter.name
+                                }}</span>
+                                <small> {{ adapter.description }}</small>
                             </div>
-                        } @else if (!adapter.running) {
-                            <button
-                                color="accent"
-                                mat-icon-button
-                                matTooltip="Start adapter"
-                                matTooltipPosition="above"
-                                data-cy="start-adapter"
-                                (click)="
-                                    startAdapter(adapter);
-                                    $event.stopPropagation()
-                                "
-                            >
-                                <i class="material-icons">play_arrow</i>
-                            </button>
-                        } @else if (adapter.running) {
-                            <button
-                                color="accent"
-                                mat-icon-button
-                                matTooltip="Stop adapter"
-                                matTooltipPosition="above"
-                                data-cy="stop-adapter"
-                                (click)="
-                                    stopAdapter(adapter);
-                                    $event.stopPropagation()
-                                "
-                            >
-                                <i class="material-icons">stop</i>
-                            </button>
-                        }
-                    </td>
-                </ng-container>
+                        </td>
+                    </ng-container>
+                    <ng-container matColumnDef="adapterBase">
+                        <th mat-header-cell *matHeaderCellDef>Adapter</th>
+                        <td mat-cell *matCellDef="let adapter">
+                            @if (getIconUrl(adapter) && !adapter.icon) {
+                                <img
+                                    class="adapter-icon"
+                                    [src]="getIconUrl(adapter)"
+                                    [alt]="adapter.name"
+                                />
+                            }
+                            @if (adapter.icon) {
+                                <img
+                                    class="adapter-icon"
+                                    [alt]="adapter.name"
+                                    [src]="adapter.icon"
+                                />
+                            }
+                        </td>
+                    </ng-container>
+                    <ng-container matColumnDef="lastModified">
+                        <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                            {{ 'Created' | translate }}
+                        </th>
+                        <td mat-cell *matCellDef="let adapter">
+                            <span>
+                                {{
+                                    adapter.createdAt | date: 'dd.MM.yyyy 
HH:mm'
+                                }}
+                            </span>
+                        </td>
+                    </ng-container>
 
-                <ng-container matColumnDef="name">
-                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                        {{ 'Name' | translate }}
-                    </th>
-                    <td mat-cell *matCellDef="let adapter">
-                        <div
-                            fxLayout="column"
-                            fxLayoutAlign="start start"
-                            class="truncate"
+                    <ng-container matColumnDef="messagesSent">
+                        <th
+                            mat-header-cell
+                            *matHeaderCellDef
+                            matTooltip="Messages sent since last start"
                         >
-                            <span data-cy="adapter-name">{{
-                                adapter.name
-                            }}</span>
-                            <small> {{ adapter.description }}</small>
-                        </div>
-                    </td>
-                </ng-container>
-                <ng-container matColumnDef="adapterBase">
-                    <th mat-header-cell *matHeaderCellDef>Adapter</th>
-                    <td mat-cell *matCellDef="let adapter">
-                        @if (getIconUrl(adapter) && !adapter.icon) {
-                            <img
-                                class="adapter-icon"
-                                [src]="getIconUrl(adapter)"
-                                [alt]="adapter.name"
-                            />
-                        }
-                        @if (adapter.icon) {
-                            <img
-                                class="adapter-icon"
-                                [alt]="adapter.name"
-                                [src]="adapter.icon"
-                            />
-                        }
-                    </td>
-                </ng-container>
-                <ng-container matColumnDef="lastModified">
-                    <th mat-header-cell mat-sort-header *matHeaderCellDef>
-                        {{ 'Created' | translate }}
-                    </th>
-                    <td mat-cell *matCellDef="let adapter">
-                        <span>
-                            {{ adapter.createdAt | date: 'dd.MM.yyyy HH:mm' }}
-                        </span>
-                    </td>
-                </ng-container>
-
-                <ng-container matColumnDef="messagesSent">
-                    <th
-                        mat-header-cell
-                        *matHeaderCellDef
-                        matTooltip="Messages sent since last start"
-                    >
-                        #{{ 'Messages' | translate }}
-                    </th>
-                    <td mat-cell *matCellDef="let adapter">
-                        <sp-label
-                            minWidth="50px"
-                            tone="neutral"
-                            size="small"
-                            shape="badge"
-                            [labelText]="
-                                adapterMetrics[adapter.elementId]?.messagesOut
-                                    .counter || 0
-                            "
-                        ></sp-label>
-                    </td>
-                </ng-container>
+                            #{{ 'Messages' | translate }}
+                        </th>
+                        <td mat-cell *matCellDef="let adapter">
+                            <sp-label
+                                minWidth="50px"
+                                tone="neutral"
+                                size="small"
+                                shape="badge"
+                                [labelText]="
+                                    adapterMetrics[adapter.elementId]
+                                        ?.messagesOut.counter || 0
+                                "
+                            ></sp-label>
+                        </td>
+                    </ng-container>
 
-                <ng-container matColumnDef="lastMessage">
-                    <th mat-header-cell *matHeaderCellDef>
-                        {{ 'Last message' | translate }}
-                    </th>
-                    <td mat-cell *matCellDef="let adapter">
-                        <span>
-                            {{
-                                adapterMetrics[adapter.elementId] &&
-                                adapterMetrics[adapter.elementId]
-                                    .lastTimestamp > 0
-                                    ? (adapterMetrics[adapter.elementId]
-                                          .lastTimestamp
-                                      | date: 'dd.MM.yyyy HH:mm')
-                                    : 'n/a'
-                            }}
-                        </span>
-                    </td>
-                </ng-container>
+                    <ng-container matColumnDef="lastMessage">
+                        <th mat-header-cell *matHeaderCellDef>
+                            {{ 'Last message' | translate }}
+                        </th>
+                        <td mat-cell *matCellDef="let adapter">
+                            <span>
+                                {{
+                                    adapterMetrics[adapter.elementId] &&
+                                    adapterMetrics[adapter.elementId]
+                                        .lastTimestamp > 0
+                                        ? (adapterMetrics[adapter.elementId]
+                                              .lastTimestamp
+                                          | date: 'dd.MM.yyyy HH:mm')
+                                        : 'n/a'
+                                }}
+                            </span>
+                        </td>
+                    </ng-container>
 
-                <ng-template spTableActions let-element>
-                    <button
-                        mat-menu-item
-                        data-cy="details-adapter"
-                        (click)="navigateToDetailsOverviewPage(element)"
-                    >
-                        <mat-icon>visibility</mat-icon>
-                        <span>{{ 'Show' | translate }}</span>
-                    </button>
-                    <button
-                        mat-menu-item
-                        data-cy="edit-adapter"
-                        (click)="editAdapter(element)"
-                    >
-                        <mat-icon>edit</mat-icon>
-                        <span>{{ 'Edit' | translate }}</span>
-                    </button>
-                    <button
-                        mat-menu-item
-                        data-cy="open-manage-permissions"
-                        (click)="showPermissionsDialog(element)"
-                    >
-                        <mat-icon>share</mat-icon>
-                        <span>{{ 'Manage permissions' | translate }}</span>
-                    </button>
-                    <button
-                        mat-menu-item
-                        data-cy="delete-adapter"
-                        (click)="deleteAdapter(element)"
-                    >
-                        <mat-icon>delete</mat-icon>
-                        <span>{{ 'Delete' | translate }}</span>
-                    </button>
-                </ng-template>
-            </sp-table>
-        </div>
+                    <ng-template spTableActions let-element>
+                        <button
+                            mat-menu-item
+                            data-cy="details-adapter"
+                            (click)="navigateToDetailsOverviewPage(element)"
+                        >
+                            <mat-icon>visibility</mat-icon>
+                            <span>{{ 'Show' | translate }}</span>
+                        </button>
+                        <button
+                            mat-menu-item
+                            data-cy="edit-adapter"
+                            (click)="editAdapter(element)"
+                        >
+                            <mat-icon>edit</mat-icon>
+                            <span>{{ 'Edit' | translate }}</span>
+                        </button>
+                        <button
+                            mat-menu-item
+                            data-cy="open-manage-permissions"
+                            (click)="showPermissionsDialog(element)"
+                        >
+                            <mat-icon>share</mat-icon>
+                            <span>{{ 'Manage permissions' | translate }}</span>
+                        </button>
+                        <button
+                            mat-menu-item
+                            data-cy="delete-adapter"
+                            (click)="deleteAdapter(element)"
+                        >
+                            <mat-icon>delete</mat-icon>
+                            <span>{{ 'Delete' | translate }}</span>
+                        </button>
+                    </ng-template>
+                </sp-table>
+            </div>
+        } @else {
+            <div fxFlex="100" fxLayout="column" class="uns-view">
+                <mat-accordion>
+                    @for (group of unsTopicGroups; track group.id) {
+                        <mat-expansion-panel [expanded]="$first">
+                            <mat-expansion-panel-header>
+                                <span class="uns-group-title">{{
+                                    group.namespace
+                                }}</span>
+                                <span class="uns-group-count">
+                                    {{ group.entries.length }}
+                                    {{ 'topics' | translate }}
+                                </span>
+                            </mat-expansion-panel-header>
+                            <sp-table
+                                fxFlex="100"
+                                featureCardId="adapter"
+                                resourceIdKey="elementId"
+                                [columns]="unsDisplayedColumns"
+                                [dataSource]="group.entries"
+                                [showActionsMenu]="false"
+                                [showSelectionCheckboxes]="false"
+                                [rowsClickable]="true"
+                                (rowClicked)="
+                                    navigateToDetailsOverviewPage(
+                                        $event.adapter
+                                    )
+                                "
+                            >
+                                <ng-container matColumnDef="topicName">
+                                    <th mat-header-cell *matHeaderCellDef>
+                                        {{ 'Topic' | translate }}
+                                    </th>
+                                    <td mat-cell *matCellDef="let entry">
+                                        <span class="topic-name">{{
+                                            entry.topicName
+                                        }}</span>
+                                    </td>
+                                </ng-container>
+                                <ng-container matColumnDef="leafSegment">
+                                    <th mat-header-cell *matHeaderCellDef>
+                                        {{ 'Leaf' | translate }}
+                                    </th>
+                                    <td mat-cell *matCellDef="let entry">
+                                        {{ entry.leafSegment }}
+                                    </td>
+                                </ng-container>
+                                <ng-container matColumnDef="adapterName">
+                                    <th mat-header-cell *matHeaderCellDef>
+                                        {{ 'Adapter' | translate }}
+                                    </th>
+                                    <td mat-cell *matCellDef="let entry">
+                                        {{ entry.adapter.name }}
+                                    </td>
+                                </ng-container>
+                                <ng-container matColumnDef="status">
+                                    <th mat-header-cell *matHeaderCellDef>
+                                        {{ 'Status' | translate }}
+                                    </th>
+                                    <td mat-cell *matCellDef="let entry">
+                                        <sp-adapter-status-light
+                                            [adapterRunning]="
+                                                entry.adapter.running
+                                            "
+                                        ></sp-adapter-status-light>
+                                    </td>
+                                </ng-container>
+                                <ng-container matColumnDef="messagesSent">
+                                    <th mat-header-cell *matHeaderCellDef>
+                                        #{{ 'Messages' | translate }}
+                                    </th>
+                                    <td mat-cell *matCellDef="let entry">
+                                        <sp-label
+                                            minWidth="50px"
+                                            tone="neutral"
+                                            size="small"
+                                            shape="badge"
+                                            [labelText]="
+                                                adapterMetrics[
+                                                    entry.adapter.elementId
+                                                ]?.messagesOut.counter || 0
+                                            "
+                                        ></sp-label>
+                                    </td>
+                                </ng-container>
+                                <ng-container matColumnDef="lastMessage">
+                                    <th mat-header-cell *matHeaderCellDef>
+                                        {{ 'Last message' | translate }}
+                                    </th>
+                                    <td mat-cell *matCellDef="let entry">
+                                        <span>
+                                            {{
+                                                adapterMetrics[
+                                                    entry.adapter.elementId
+                                                ] &&
+                                                adapterMetrics[
+                                                    entry.adapter.elementId
+                                                ].lastTimestamp > 0
+                                                    ? (adapterMetrics[
+                                                          entry.adapter
+                                                              .elementId
+                                                      ].lastTimestamp
+                                                      | date
+                                                          : 'dd.MM.yyyy HH:mm')
+                                                    : 'n/a'
+                                            }}
+                                        </span>
+                                    </td>
+                                </ng-container>
+                            </sp-table>
+                        </mat-expansion-panel>
+                    }
+                </mat-accordion>
+            </div>
+        }
     </div>
 </sp-basic-view>
diff --git 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.scss
 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.scss
index f47cad0e28..fc71abffcf 100644
--- 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.scss
+++ 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.scss
@@ -39,3 +39,33 @@
 .icon {
     max-width: 60px;
 }
+
+.overview-mode-toggle {
+    margin-left: 10px;
+    flex: 0 0 auto;
+    width: max-content;
+}
+
+.overview-mode-toggle .mat-button-toggle {
+    min-width: 72px;
+}
+
+.topic-name {
+    font-family: monospace;
+    word-break: break-all;
+}
+
+.uns-view {
+    margin-top: 10px;
+}
+
+.uns-group-title {
+    font-weight: 600;
+    font-family: monospace;
+}
+
+.uns-group-count {
+    margin-left: auto;
+    color: rgba(0, 0, 0, 0.6);
+    font-size: 12px;
+}
diff --git 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts
 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts
index cba1c1f980..2f15b1bc12 100644
--- 
a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts
+++ 
b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.ts
@@ -75,6 +75,31 @@ import { AdapterStatusLightComponent } from 
'./adapter-status-light/adapter-stat
 import { MatProgressSpinner } from '@angular/material/progress-spinner';
 import { MatMenuItem } from '@angular/material/menu';
 import { DatePipe } from '@angular/common';
+import {
+    MatButtonToggle,
+    MatButtonToggleGroup,
+} from '@angular/material/button-toggle';
+import {
+    MatAccordion,
+    MatExpansionPanel,
+    MatExpansionPanelHeader,
+} from '@angular/material/expansion';
+
+type AdapterOverviewMode = 'table' | 'uns';
+
+interface UnsTopicEntry {
+    elementId: string;
+    adapter: AdapterDescription;
+    topicName: string;
+    namespace: string;
+    leafSegment: string;
+}
+
+interface UnsTopicGroup {
+    id: string;
+    namespace: string;
+    entries: UnsTopicEntry[];
+}
 
 @Component({
     selector: 'sp-existing-adapters',
@@ -106,6 +131,11 @@ import { DatePipe } from '@angular/common';
         SpTableActionsDirective,
         MatMenuItem,
         DatePipe,
+        MatButtonToggleGroup,
+        MatButtonToggle,
+        MatAccordion,
+        MatExpansionPanel,
+        MatExpansionPanelHeader,
         TranslatePipe,
     ],
 })
@@ -130,6 +160,14 @@ export class ExistingAdaptersComponent implements OnInit, 
OnDestroy {
         'lastMessage',
         'actions',
     ];
+    unsDisplayedColumns: string[] = [
+        'topicName',
+        'leafSegment',
+        'adapterName',
+        'status',
+        'messagesSent',
+        'lastMessage',
+    ];
     readonly assetContextConfig: SpTableAssetContextConfig = {
         resourceLinkType: 'adapter',
         resourceIdKey: 'elementId',
@@ -152,6 +190,8 @@ export class ExistingAdaptersComponent implements OnInit, 
OnDestroy {
 
     startAdapterErrorText = 'Could not start adapter';
     stopAdapterErrorText = 'Could not stop adapter';
+    overviewMode: AdapterOverviewMode = 'table';
+    unsTopicGroups: UnsTopicGroup[] = [];
 
     private adapterService = inject(AdapterService);
     private dialogService = inject(DialogService);
@@ -379,6 +419,7 @@ export class ExistingAdaptersComponent implements OnInit, 
OnDestroy {
                 }
             });
         this.dataSource.data = this.filteredAdapters;
+        this.unsTopicGroups = this.buildUnsTopicGroups(this.filteredAdapters);
     }
 
     startAdapterTutorial() {
@@ -402,6 +443,75 @@ export class ExistingAdaptersComponent implements OnInit, 
OnDestroy {
         this.router.navigate(['connect', 'details', adapter.elementId]);
     }
 
+    setOverviewMode(mode: AdapterOverviewMode): void {
+        this.overviewMode = mode;
+    }
+
+    getTopicName(adapter: AdapterDescription): string {
+        return (
+            adapter.eventGrounding?.transportProtocols?.[0]?.topicDefinition
+                ?.actualTopicName || ''
+        );
+    }
+
+    private buildUnsTopicGroups(
+        adapters: AdapterDescription[],
+    ): UnsTopicGroup[] {
+        const groupedTopics = new Map<string, UnsTopicEntry[]>();
+
+        adapters.forEach(adapter => {
+            const topicName = this.getTopicName(adapter);
+            const segments = this.getTopicSegments(topicName);
+            const namespace =
+                segments.length > 1
+                    ? segments
+                          .slice(0, -1)
+                          .join(this.getTopicDelimiter(topicName))
+                    : this.translate.instant('Ungrouped topics');
+            const leafSegment =
+                segments.length > 0 ? segments[segments.length - 1] : 
topicName;
+
+            const entry: UnsTopicEntry = {
+                elementId: adapter.elementId,
+                adapter,
+                topicName,
+                namespace,
+                leafSegment,
+            };
+
+            if (!groupedTopics.has(namespace)) {
+                groupedTopics.set(namespace, []);
+            }
+
+            groupedTopics.get(namespace).push(entry);
+        });
+
+        return Array.from(groupedTopics.entries())
+            .sort(([namespaceA], [namespaceB]) =>
+                namespaceA.localeCompare(namespaceB),
+            )
+            .map(([namespace, entries]) => ({
+                id: namespace,
+                namespace,
+                entries: entries.sort((a, b) =>
+                    a.topicName.localeCompare(b.topicName),
+                ),
+            }));
+    }
+
+    private getTopicSegments(topicName: string): string[] {
+        if (!topicName) {
+            return [];
+        }
+
+        const delimiter = this.getTopicDelimiter(topicName);
+        return topicName.split(delimiter).filter(segment => segment.length > 
0);
+    }
+
+    private getTopicDelimiter(topicName: string): string {
+        return topicName.includes('/') ? '/' : '.';
+    }
+
     ngOnDestroy() {
         this.user$?.unsubscribe();
         this.tutorial$?.unsubscribe();


Reply via email to