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

riemer pushed a commit to branch 4190-support-multi-select-actions-in-tables
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/4190-support-multi-select-actions-in-tables by this push:
     new 02c94d18ca feat(#4190): Add multi-select actions to pipelines
02c94d18ca is described below

commit 02c94d18ca3555660d7329651a546ae6086d98ba
Author: Dominik Riemer <[email protected]>
AuthorDate: Mon Feb 23 09:22:40 2026 +0100

    feat(#4190): Add multi-select actions to pipelines
---
 ...nt.scss => sp-table-multi-actions.directive.ts} |  25 +-
 .../components/sp-table/sp-table.component.html    | 142 ++++++++++-
 .../components/sp-table/sp-table.component.scss    |  10 +-
 .../lib/components/sp-table/sp-table.component.ts  | 272 ++++++++++++++++++++-
 .../streampipes/shared-ui/src/public-api.ts        |   1 +
 .../pipeline-overview.component.html               |   5 +-
 .../pipeline-overview.component.ts                 |  70 ++++++
 .../start-all-pipelines-dialog.component.ts        |   5 +-
 ui/src/scss/sp/forms.scss                          |  12 +
 9 files changed, 512 insertions(+), 30 deletions(-)

diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-multi-actions.directive.ts
similarity index 65%
copy from 
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
copy to 
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-multi-actions.directive.ts
index 4d0b280941..21511f378f 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-multi-actions.directive.ts
@@ -1,4 +1,4 @@
-/*!
+/*
  * 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.
@@ -16,24 +16,7 @@
  *
  */
 
-.paginator-container {
-    border-top: 1px solid rgba(0, 0, 0, 0.12);
-}
+import { Directive } from '@angular/core';
 
-.mat-mdc-row:hover {
-    background-color: var(--color-bg-1);
-}
-
-.mat-mdc-no-data-row {
-    height: var(--mat-table-row-item-container-height, 52px);
-    text-align: center;
-}
-
-.cursor-pointer {
-    cursor: pointer;
-}
-
-.right-column {
-    text-align: right; /* align contents inside cell */
-    margin-left: auto; /* push this column to the far right */
-}
+@Directive({ selector: 'ng-template[spTableMultiActions]' })
+export class SpTableMultiActionsDirective {}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
index 340dfff202..86ba8b3f4e 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
@@ -17,9 +17,145 @@
   -->
 
 <div fxLayout="column">
+    @if (showSelectionCheckboxes) {
+        <div
+            class="selection-toolbar"
+            fxLayout="row wrap"
+            fxLayoutAlign="space-between center"
+            fxLayoutGap="8px"
+        >
+            <div
+                fxLayout="row wrap"
+                fxLayoutAlign="start center"
+                fxLayoutGap="8px"
+            >
+                <div
+                    fxLayout="row wrap"
+                    fxLayoutAlign="start center"
+                    fxLayoutGap="4px"
+                >
+                    <button
+                        mat-flat-button
+                        class="mat-basic"
+                        (click)="selectVisiblePageRows()"
+                        [disabled]="!visiblePageRows.length"
+                    >
+                        <mat-icon>select_all</mat-icon>
+                        {{ 'Select visible' | translate }}
+                    </button>
+                    <button
+                        mat-flat-button
+                        class="mat-basic"
+                        (click)="clearSelection()"
+                        [disabled]="!selectedRows.length"
+                    >
+                        <mat-icon>deselect</mat-icon>
+                        {{ 'Select none' | translate }}
+                    </button>
+                </div>
+            </div>
+
+            @if (hasMultiActionsToolbarControls()) {
+                <div fxLayout="row" fxLayoutGap="5px">
+                    @if (hasBuiltInMultiActionSelect()) {
+                        <sp-form-field
+                            [level]="3"
+                            [label]="multiActionsSelectLabel | translate"
+                            margin="0"
+                        >
+                            <mat-form-field class="form-field-small">
+                                <mat-select
+                                    panelClass="small-select-panel"
+                                    [placeholder]="'Select action' | translate"
+                                    [value]="selectedMultiAction"
+                                    (valueChange)="
+                                        onSelectedMultiActionChange($event)
+                                    "
+                                >
+                                    @for (
+                                        actionOption of multiActionOptions;
+                                        track actionOption.value
+                                    ) {
+                                        <mat-option
+                                            [value]="actionOption.value"
+                                            [disabled]="actionOption.disabled"
+                                        >
+                                            @if (actionOption.icon) {
+                                                <mat-icon
+                                                    
class="selection-toolbar__action-option-icon"
+                                                >
+                                                    {{ actionOption.icon }}
+                                                </mat-icon>
+                                            }
+                                            {{ actionOption.label | translate 
}}
+                                        </mat-option>
+                                    }
+                                </mat-select>
+                            </mat-form-field>
+                        </sp-form-field>
+                    }
+
+                    @if (multiActionsTemplate) {
+                        <ng-container
+                            *ngTemplateOutlet="
+                                multiActionsTemplate;
+                                context: multiActionsContext
+                            "
+                        >
+                        </ng-container>
+                    }
+
+                    @if (showMultiActionsExecuteButton) {
+                        <sp-form-field [level]="3" label="&nbsp;" margin="0">
+                            <button
+                                mat-flat-button
+                                (click)="emitMultiActionsExecute()"
+                                [disabled]="
+                                    isMultiActionsExecuteButtonDisabled()
+                                "
+                            >
+                                {{ multiActionsExecuteLabel | translate }}
+                            </button>
+                        </sp-form-field>
+                    }
+                </div>
+            }
+        </div>
+    }
+
     <table mat-table class="sp-table" [dataSource]="dataSource">
         <ng-content></ng-content>
 
+        @if (showSelectionCheckboxes) {
+            <ng-container [matColumnDef]="selectionColumnId">
+                <th
+                    mat-header-cell
+                    *matHeaderCellDef
+                    class="checkbox-multi-select"
+                >
+                    <mat-checkbox
+                        [checked]="areAllVisibleRowsSelected()"
+                        [indeterminate]="areSomeVisibleRowsSelected()"
+                        (change)="toggleSelectAllVisibleRows($event.checked)"
+                        (click)="$event.stopPropagation()"
+                    >
+                    </mat-checkbox>
+                </th>
+                <td
+                    mat-cell
+                    *matCellDef="let element"
+                    class="checkbox-multi-select"
+                >
+                    <mat-checkbox
+                        [checked]="isRowSelected(element)"
+                        (change)="toggleRowSelection(element, $event.checked)"
+                        (click)="$event.stopPropagation()"
+                    >
+                    </mat-checkbox>
+                </td>
+            </ng-container>
+        }
+
         @if (showActionsMenu) {
             <ng-container matColumnDef="actions">
                 <th mat-header-cell *matHeaderCellDef></th>
@@ -72,10 +208,10 @@
             </ng-container>
         }
 
-        <tr mat-header-row *matHeaderRowDef="columns"></tr>
+        <tr mat-header-row *matHeaderRowDef="renderedColumns"></tr>
         <tr
             mat-row
-            *matRowDef="let row; columns: columns"
+            *matRowDef="let row; columns: renderedColumns"
             (click)="rowClicked.emit(row)"
             [ngClass]="rowsClickable ? 'cursor-pointer' : ''"
         ></tr>
@@ -84,7 +220,7 @@
             <td
                 data-cy="no-table-entries"
                 class="mat-cell"
-                [colSpan]="columns.length"
+                [colSpan]="renderedColumns.length"
             >
                 {{ 'No entries available.' | translate }}
             </td>
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
index 4d0b280941..e58390be1f 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
@@ -17,7 +17,11 @@
  */
 
 .paginator-container {
-    border-top: 1px solid rgba(0, 0, 0, 0.12);
+    border-top: 1px solid var(--color-bg-3);
+}
+
+.selection-toolbar {
+    border-bottom: 1px solid var(--color-bg-3);
 }
 
 .mat-mdc-row:hover {
@@ -37,3 +41,7 @@
     text-align: right; /* align contents inside cell */
     margin-left: auto; /* push this column to the far right */
 }
+
+.checkbox-multi-select {
+    width: 100px;
+}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
index f750f75381..bdae3abb34 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
@@ -25,12 +25,16 @@ import {
     EventEmitter,
     inject,
     Input,
+    OnChanges,
+    OnDestroy,
     Output,
     QueryList,
     Signal,
+    SimpleChanges,
     TemplateRef,
     ViewChild,
 } from '@angular/core';
+import { SelectionModel } from '@angular/cdk/collections';
 import {
     MatCell,
     MatCellDef,
@@ -48,6 +52,7 @@ import {
 import { MatPaginator, PageEvent } from '@angular/material/paginator';
 import { SpTableActionsDirective } from './sp-table-actions.directive';
 import { MatMenu, MatMenuTrigger } from '@angular/material/menu';
+import { SpTableMultiActionsDirective } from 
'./sp-table-multi-actions.directive';
 import { LocalStorageService } from 
'../../services/local-storage-settings.service';
 import { FeatureCardService } from '../feature-card-host/feature-card.service';
 import {
@@ -55,12 +60,30 @@ import {
     LayoutAlignDirective,
     LayoutDirective,
 } from '@ngbracket/ngx-layout/flex';
-import { MatIconButton } from '@angular/material/button';
+import { LayoutGapDirective } from '@ngbracket/ngx-layout';
+import { MatButton, MatIconButton } from '@angular/material/button';
 import { MatTooltip } from '@angular/material/tooltip';
 import { MatIcon } from '@angular/material/icon';
 import { NgClass, NgTemplateOutlet } from '@angular/common';
 import { ClassDirective } from '@ngbracket/ngx-layout/extended';
 import { TranslatePipe } from '@ngx-translate/core';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { MatFormField } from '@angular/material/form-field';
+import { Subscription } from 'rxjs';
+import { MatOption, MatSelect } from '@angular/material/select';
+import { FormFieldComponent } from '../form-field/form-field.component';
+
+export interface SpTableMultiActionOption {
+    value: string;
+    label: string;
+    icon?: string;
+    disabled?: boolean;
+}
+
+export interface SpTableMultiActionExecuteEvent<T> {
+    selectedRows: T[];
+    action: string | null;
+}
 
 @Component({
     selector: 'sp-table',
@@ -76,10 +99,15 @@ import { TranslatePipe } from '@ngx-translate/core';
         MatCell,
         LayoutAlignDirective,
         MatIconButton,
+        MatButton,
         MatTooltip,
         MatIcon,
+        MatCheckbox,
+        MatFormField,
         MatMenuTrigger,
         MatMenu,
+        MatSelect,
+        MatOption,
         NgTemplateOutlet,
         MatHeaderRowDef,
         MatHeaderRow,
@@ -91,9 +119,15 @@ import { TranslatePipe } from '@ngx-translate/core';
         FlexDirective,
         MatPaginator,
         TranslatePipe,
+        LayoutGapDirective,
+        FormFieldComponent,
     ],
 })
-export class SpTableComponent<T> implements AfterViewInit, AfterContentInit {
+export class SpTableComponent<T>
+    implements AfterViewInit, AfterContentInit, OnChanges, OnDestroy
+{
+    readonly selectionColumnId = 'spSelection';
+
     @ContentChildren(MatHeaderRowDef) headerRowDefs: 
QueryList<MatHeaderRowDef>;
     @ContentChildren(MatRowDef) rowDefs: QueryList<MatRowDef<T>>;
     @ContentChildren(MatColumnDef) columnDefs: QueryList<MatColumnDef>;
@@ -104,22 +138,41 @@ export class SpTableComponent<T> implements 
AfterViewInit, AfterContentInit {
     @Input() columns: string[];
     @Input() rowsClickable = false;
     @Input() showActionsMenu = false;
+    @Input() showSelectionCheckboxes = false;
+    @Input() showMultiActionsExecuteButton = false;
+    @Input() multiActionsExecuteLabel = 'Execute';
+    @Input() multiActionsExecuteDisabled = false;
+    @Input() multiActionsSelectLabel = 'Action';
+    @Input() multiActionOptions: SpTableMultiActionOption[] = [];
     @Input() featureCardId: string;
     @Input() resourceIdKey = 'elementId';
 
     @Input() dataSource: MatTableDataSource<T>;
 
     @Output() rowClicked = new EventEmitter<T>();
+    @Output() selectionChanged = new EventEmitter<T[]>();
+    @Output() multiActionsExecute = new EventEmitter<
+        SpTableMultiActionExecuteEvent<T>
+    >();
+    @Output() multiActionSelectionChanged = new EventEmitter<string | null>();
 
     @ViewChild('paginator') paginator: MatPaginator;
     @ContentChild(SpTableActionsDirective, { read: TemplateRef })
     actionsTemplate?: TemplateRef<any>;
+    @ContentChild(SpTableMultiActionsDirective, { read: TemplateRef })
+    multiActionsTemplate?: TemplateRef<any>;
 
     timedOutCloser: any;
     trigger: MatMenuTrigger | undefined = undefined;
+    visiblePageRows: T[] = [];
+    selectedMultiAction: string | null = null;
+
+    readonly selection = new SelectionModel<T>(true, []);
 
     private localStorageService = inject(LocalStorageService);
     private featureCardService = inject(FeatureCardService);
+    private renderedDataSubscription?: Subscription;
+    private viewInitialized = false;
 
     readonly pageSize: Signal<number>;
 
@@ -131,7 +184,8 @@ export class SpTableComponent<T> implements AfterViewInit, 
AfterContentInit {
     }
 
     ngAfterViewInit() {
-        this.dataSource.paginator = this.paginator;
+        this.viewInitialized = true;
+        this.bindDataSource();
     }
 
     ngAfterContentInit() {
@@ -145,6 +199,34 @@ export class SpTableComponent<T> implements AfterViewInit, 
AfterContentInit {
         this.table.setNoDataRow(this.noDataRow);
     }
 
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['dataSource']) {
+            this.selection.clear();
+            this.emitSelection();
+            this.visiblePageRows = [];
+            if (this.viewInitialized) {
+                this.bindDataSource();
+            }
+        }
+
+        if (
+            changes['showSelectionCheckboxes'] &&
+            !this.showSelectionCheckboxes &&
+            this.selection.hasValue()
+        ) {
+            this.selection.clear();
+            this.emitSelection();
+        }
+
+        if (changes['multiActionOptions']) {
+            this.ensureValidSelectedMultiAction();
+        }
+    }
+
+    ngOnDestroy() {
+        this.renderedDataSubscription?.unsubscribe();
+    }
+
     mouseEnter(trigger) {
         if (this.timedOutCloser) {
             clearTimeout(this.timedOutCloser);
@@ -173,4 +255,188 @@ export class SpTableComponent<T> implements 
AfterViewInit, AfterContentInit {
             element[this.resourceIdKey],
         );
     }
+
+    get renderedColumns(): string[] {
+        const baseColumns = this.columns ?? [];
+        if (
+            !this.showSelectionCheckboxes ||
+            baseColumns.includes(this.selectionColumnId)
+        ) {
+            return baseColumns;
+        }
+
+        return [this.selectionColumnId, ...baseColumns];
+    }
+
+    get selectedRows(): T[] {
+        return this.selection.selected;
+    }
+
+    get multiActionsContext() {
+        return {
+            $implicit: this.selectedRows,
+            selectedRows: this.selectedRows,
+            selectedCount: this.selectedRows.length,
+            visiblePageRows: this.visiblePageRows,
+            visiblePageRowCount: this.visiblePageRows.length,
+        };
+    }
+
+    hasBuiltInMultiActionSelect(): boolean {
+        return this.multiActionOptions?.length > 0;
+    }
+
+    hasMultiActionsToolbarControls(): boolean {
+        return (
+            this.hasBuiltInMultiActionSelect() ||
+            !!this.multiActionsTemplate ||
+            this.showMultiActionsExecuteButton
+        );
+    }
+
+    isRowSelected(row: T): boolean {
+        return this.selection.isSelected(row);
+    }
+
+    toggleRowSelection(row: T, checked: boolean) {
+        if (checked) {
+            this.selection.select(row);
+        } else {
+            this.selection.deselect(row);
+        }
+
+        this.emitSelection();
+    }
+
+    selectVisiblePageRows() {
+        if (!this.visiblePageRows.length) {
+            return;
+        }
+
+        this.selection.select(...this.visiblePageRows);
+        this.emitSelection();
+    }
+
+    clearSelection() {
+        if (!this.selection.hasValue()) {
+            return;
+        }
+
+        this.selection.clear();
+        this.emitSelection();
+    }
+
+    toggleSelectAllVisibleRows(checked: boolean) {
+        if (checked) {
+            this.selectVisiblePageRows();
+            return;
+        }
+
+        if (!this.visiblePageRows.length) {
+            return;
+        }
+
+        this.selection.deselect(...this.visiblePageRows);
+        this.emitSelection();
+    }
+
+    areAllVisibleRowsSelected(): boolean {
+        return (
+            this.visiblePageRows.length > 0 &&
+            this.visiblePageRows.every(row => this.selection.isSelected(row))
+        );
+    }
+
+    areSomeVisibleRowsSelected(): boolean {
+        return (
+            this.visiblePageRows.some(row => this.selection.isSelected(row)) &&
+            !this.areAllVisibleRowsSelected()
+        );
+    }
+
+    private bindDataSource() {
+        if (!this.dataSource || !this.paginator) {
+            return;
+        }
+
+        this.dataSource.paginator = this.paginator;
+
+        this.renderedDataSubscription?.unsubscribe();
+        this.renderedDataSubscription = this.dataSource.connect().subscribe({
+            next: rows => {
+                this.visiblePageRows = rows ?? [];
+                this.pruneSelection();
+            },
+        });
+    }
+
+    private pruneSelection() {
+        if (!this.selection.hasValue() || !this.dataSource) {
+            return;
+        }
+
+        const availableRows = new Set(this.dataSource.filteredData ?? []);
+        const rowsToRemove = this.selection.selected.filter(
+            row => !availableRows.has(row),
+        );
+
+        if (!rowsToRemove.length) {
+            return;
+        }
+
+        this.selection.deselect(...rowsToRemove);
+        this.emitSelection();
+    }
+
+    private emitSelection() {
+        this.selectionChanged.emit(this.selection.selected);
+    }
+
+    emitMultiActionsExecute() {
+        this.multiActionsExecute.emit({
+            selectedRows: this.selection.selected,
+            action: this.selectedMultiAction,
+        });
+    }
+
+    onSelectedMultiActionChange(action: string | null) {
+        this.selectedMultiAction = action;
+        this.multiActionSelectionChanged.emit(action);
+    }
+
+    isMultiActionsExecuteButtonDisabled(): boolean {
+        if (
+            !this.selection.selected.length ||
+            this.multiActionsExecuteDisabled
+        ) {
+            return true;
+        }
+
+        if (this.hasBuiltInMultiActionSelect() && !this.selectedMultiAction) {
+            return true;
+        }
+
+        const selectedOption = this.multiActionOptions?.find(
+            option => option.value === this.selectedMultiAction,
+        );
+
+        return !!selectedOption?.disabled;
+    }
+
+    private ensureValidSelectedMultiAction() {
+        if (!this.selectedMultiAction) {
+            return;
+        }
+
+        const actionStillExists = (this.multiActionOptions ?? []).some(
+            option => option.value === this.selectedMultiAction,
+        );
+
+        if (actionStillExists) {
+            return;
+        }
+
+        this.selectedMultiAction = null;
+        this.multiActionSelectionChanged.emit(null);
+    }
 }
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts 
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index 94f476cbe4..5b4a0b2060 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -44,6 +44,7 @@ export * from 
'./lib/components/sp-exception-message/exception-details/exception
 export * from './lib/components/sp-label/sp-label.component';
 export * from './lib/components/sp-table/sp-table.component';
 export * from './lib/components/sp-table/sp-table-actions.directive';
+export * from './lib/components/sp-table/sp-table-multi-actions.directive';
 export * from './lib/components/alert-banner/alert-banner.component';
 export * from './lib/components/time-selector/time-selector.model';
 export * from './lib/components/time-selector/time-range-selector.component';
diff --git 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
index 4d41790d41..684769168c 100644
--- 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
+++ 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
@@ -19,10 +19,14 @@
 <sp-table
     [dataSource]="dataSource"
     [columns]="displayedColumns"
+    [showSelectionCheckboxes]="hasPipelineWritePrivileges"
+    [showMultiActionsExecuteButton]="true"
+    [multiActionOptions]="bulkPipelineActionOptions"
     featureCardId="pipeline"
     resourceIdKey="_id"
     [showActionsMenu]="true"
     [rowsClickable]="true"
+    (multiActionsExecute)="executeSelectedPipelineAction($event)"
     (rowClicked)="
         pipelineOperationsService.showPipelineDetails($event.elementId)
     "
@@ -133,7 +137,6 @@
                 }
                 @if (pipeline.running) {
                     <button
-                        color="accent"
                         mat-icon-button
                         [matTooltip]="'Stop pipeline' | translate"
                         matTooltipPosition="above"
diff --git 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
index 299c77a774..e37e1a43ab 100644
--- 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
+++ 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
@@ -27,6 +27,7 @@ import {
     Output,
     ViewChild,
 } from '@angular/core';
+import { StartAllPipelinesDialogComponent } from 
'../../dialog/start-all-pipelines/start-all-pipelines-dialog.component';
 import { PipelineOperationsService } from 
'../../services/pipeline-operations.service';
 import {
     MatCell,
@@ -41,6 +42,11 @@ import { AuthService } from '../../../services/auth.service';
 import { UserPrivilege } from '../../../_enums/user-privilege.enum';
 import {
     CurrentUserService,
+    DialogRef,
+    DialogService,
+    PanelType,
+    SpTableMultiActionExecuteEvent,
+    SpTableMultiActionOption,
     SpTableActionsDirective,
     SpTableComponent,
 } from '@streampipes/shared-ui';
@@ -105,12 +111,18 @@ export class PipelineOverviewComponent implements OnInit, 
OnDestroy {
     starting = false;
     stopping = false;
     hasPipelineWritePrivileges = false;
+    readonly bulkPipelineActionOptions: SpTableMultiActionOption[] = [
+        { value: 'start', label: 'Start selected', icon: 'play_arrow' },
+        { value: 'stop', label: 'Stop selected', icon: 'stop' },
+        { value: 'forceStop', label: 'Force stop selected', icon: 'stop' },
+    ];
 
     userSub: Subscription;
 
     public pipelineOperationsService = inject(PipelineOperationsService);
     private authService = inject(AuthService);
     private currentUserService = inject(CurrentUserService);
+    private dialogService = inject(DialogService);
 
     ngOnInit() {
         this.userSub = this.currentUserService.user$.subscribe(user => {
@@ -161,6 +173,64 @@ export class PipelineOverviewComponent implements OnInit, 
OnDestroy {
         });
     }
 
+    startStopSelectedPipelines(
+        selectedPipelines: Pipeline[],
+        action: boolean,
+        forceStop = false,
+    ) {
+        const pipelines = selectedPipelines.filter(pipeline =>
+            action ? !pipeline.running && pipeline.valid : pipeline.running,
+        );
+
+        if (!pipelines.length) {
+            return;
+        }
+
+        const dialogRef: DialogRef<StartAllPipelinesDialogComponent> =
+            this.dialogService.open(StartAllPipelinesDialogComponent, {
+                panelType: PanelType.STANDARD_PANEL,
+                title: (action ? 'Start' : 'Stop') + ' selected pipelines',
+                width: '70vw',
+                data: {
+                    pipelines,
+                    action,
+                    forceStop,
+                },
+            });
+
+        dialogRef.afterClosed().subscribe(refresh => {
+            if (refresh) {
+                this.refreshPipelinesEmitter.emit(true);
+            }
+        });
+    }
+
+    executeSelectedPipelineAction(
+        event: SpTableMultiActionExecuteEvent<Pipeline>,
+    ) {
+        if (
+            !this.hasPipelineWritePrivileges ||
+            this.starting ||
+            this.stopping
+        ) {
+            return;
+        }
+
+        if (
+            event.action !== 'start' &&
+            event.action !== 'stop' &&
+            event.action !== 'forceStop'
+        ) {
+            return;
+        }
+
+        this.startStopSelectedPipelines(
+            event.selectedRows,
+            event.action === 'start',
+            event.action === 'forceStop',
+        );
+    }
+
     ngOnDestroy() {
         this.userSub?.unsubscribe();
     }
diff --git 
a/ui/src/app/pipelines/dialog/start-all-pipelines/start-all-pipelines-dialog.component.ts
 
b/ui/src/app/pipelines/dialog/start-all-pipelines/start-all-pipelines-dialog.component.ts
index 0b9d576e02..3d62cda9ce 100644
--- 
a/ui/src/app/pipelines/dialog/start-all-pipelines/start-all-pipelines-dialog.component.ts
+++ 
b/ui/src/app/pipelines/dialog/start-all-pipelines/start-all-pipelines-dialog.component.ts
@@ -33,6 +33,9 @@ export class StartAllPipelinesDialogComponent implements 
OnInit {
     @Input()
     pipelines: Pipeline[];
 
+    @Input()
+    forceStop = false;
+
     pipelinesToModify: Pipeline[];
     installationStatus: any;
     installationFinished: boolean;
@@ -133,7 +136,7 @@ export class StartAllPipelinesDialogComponent implements 
OnInit {
 
     stopPipeline(pipeline, index) {
         this.pipelineService
-            .stopPipeline(pipeline._id)
+            .stopPipeline(pipeline._id, this.forceStop)
             .subscribe(
                 data => {
                     this.installationStatus[index].status = data.success
diff --git a/ui/src/scss/sp/forms.scss b/ui/src/scss/sp/forms.scss
index bd31a182e5..335228e26c 100644
--- a/ui/src/scss/sp/forms.scss
+++ b/ui/src/scss/sp/forms.scss
@@ -97,6 +97,18 @@ mat-form-field.mat-mdc-form-field.form-field-size-smaller {
     }
 }
 
+.small-select-panel {
+    --mat-option-label-text-size: var(--font-size-md);
+    font-size: var(--font-size-md);
+
+    .mat-icon {
+        width: var(--font-size-md);
+        height: var(--font-size-md);
+        font-size: var(--font-size-md);
+        line-height: var(--font-size-md);
+    }
+}
+
 .form-field-smaller {
     .mat-mdc-form-field-input-control.mat-mdc-form-field-input-control {
         letter-spacing: 0;

Reply via email to