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