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

riemer pushed a commit to branch add-split-button
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit e412749143f70b81891310bb2ef29e0cb4aa13cc
Author: Dominik Riemer <[email protected]>
AuthorDate: Tue Mar 24 17:56:00 2026 +0100

    feat: Add split button
---
 .../asset-link-configuration.component.html        |   9 +-
 .../asset-link-configuration.component.ts          |   4 +
 .../split-button/split-button.component.html       |  83 ++++++++++++++
 .../split-button/split-button.component.scss       | 125 +++++++++++++++++++++
 .../split-button/split-button.component.ts         | 118 +++++++++++++++++++
 .../streampipes/shared-ui/src/public-api.ts        |   1 +
 6 files changed, 339 insertions(+), 1 deletion(-)

diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.html
index 25bfcf49e0..368ac42e67 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.html
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.html
@@ -82,7 +82,14 @@
     <!-- If no assets available -->
     @if (!assetsData?.length) {
         <div>
-            <p>No assets available</p>
+            <sp-alert-banner
+                type="info"
+                [title]="'No assets available' | translate"
+                [description]="
+                    'Create a new asset in the asset view before adding it to 
a resource.'
+                        | translate
+                "
+            ></sp-alert-banner>
         </div>
     }
 }
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
index cde0c9e4d5..fc66c27748 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/asset-link-configuration/asset-link-configuration.component.ts
@@ -45,6 +45,8 @@ import { MatStepper } from '@angular/material/stepper';
 import { MatIconButton } from '@angular/material/button';
 import { MatIcon } from '@angular/material/icon';
 import { LayoutAlignDirective } from '@ngbracket/ngx-layout/flex';
+import { SpAlertBannerComponent } from 
'../alert-banner/alert-banner.component';
+import { TranslatePipe } from '@ngx-translate/core';
 
 @Component({
     selector: 'sp-asset-link-configuration',
@@ -60,6 +62,8 @@ import { LayoutAlignDirective } from 
'@ngbracket/ngx-layout/flex';
         LayoutAlignDirective,
         MatTreeNodeOutlet,
         MatTreeNode,
+        SpAlertBannerComponent,
+        TranslatePipe,
     ],
 })
 export class AssetLinkConfigurationComponent implements OnInit {
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.html
new file mode 100644
index 0000000000..bce26a4533
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.html
@@ -0,0 +1,83 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+<div
+    class="split-button"
+    cdkOverlayOrigin
+    #overlayOrigin="cdkOverlayOrigin"
+    [class.split-button--basic]="appearance === 'mat-basic'"
+    [class.split-button--open]="menuOpen"
+>
+    <button
+        mat-flat-button
+        [type]="buttonType"
+        class="split-button__main"
+        [class.mat-basic]="appearance === 'mat-basic'"
+        [disabled]="disabled"
+        (click)="onPrimaryActionClicked()"
+    >
+        @if (icon) {
+            <mat-icon>{{ icon }}</mat-icon>
+        }
+        <span>{{ label }}</span>
+    </button>
+
+    <button
+        mat-flat-button
+        type="button"
+        class="split-button__toggle"
+        [class.mat-basic]="appearance === 'mat-basic'"
+        [attr.aria-label]="menuAriaLabel | translate"
+        [disabled]="isDropdownDisabled()"
+        (click)="toggleMenu($event)"
+    >
+        <mat-icon>arrow_drop_down</mat-icon>
+    </button>
+</div>
+
+<ng-template
+    cdkConnectedOverlay
+    [cdkConnectedOverlayOrigin]="overlayOrigin"
+    [cdkConnectedOverlayOpen]="menuOpen"
+    [cdkConnectedOverlayPositions]="overlayPositions"
+    [cdkConnectedOverlayHasBackdrop]="true"
+    cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
+    (backdropClick)="closeMenu()"
+    (detach)="closeMenu()"
+>
+    <div
+        class="split-button__menu-panel"
+        [style.width.px]="menuWidth"
+        role="menu"
+    >
+        @for (action of actions; track action.action) {
+            <button
+                type="button"
+                class="split-button__menu-item"
+                [disabled]="action.disabled"
+                (click)="onSplitActionClicked(action)"
+                role="menuitem"
+            >
+                @if (action.icon) {
+                    <mat-icon>{{ action.icon }}</mat-icon>
+                }
+                <span>{{ action.label }}</span>
+            </button>
+        }
+    </div>
+</ng-template>
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.scss
 
b/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.scss
new file mode 100644
index 0000000000..accf1236b0
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.scss
@@ -0,0 +1,125 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+:host {
+    display: inline-flex;
+}
+
+.split-button {
+    display: inline-flex;
+    align-items: stretch;
+    gap: 2px;
+}
+
+.split-button__main,
+.split-button__toggle {
+    margin: 0;
+    transition: border-radius 150ms ease;
+}
+
+.split-button__main {
+    border-radius: 999px 4px 4px 999px;
+}
+
+.split-button__main mat-icon {
+    margin-right: 8px;
+}
+
+.split-button__toggle {
+    min-width: 2.5rem;
+    width: 2.5rem;
+    padding: 0;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 4px 999px 999px 4px;
+}
+
+.split-button__toggle mat-icon {
+    margin: 0;
+}
+
+.split-button--basic .split-button__toggle {
+    border-color: var(--mat-sys-surface-container-high);
+}
+
+.split-button__menu-panel {
+    position: relative;
+    margin-top: 2px;
+    border-radius: 4px;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    padding: 8px 0;
+    background: var(--mat-sys-surface);
+    color: var(--mat-sys-on-surface);
+    box-shadow:
+        0 2px 6px rgba(0, 0, 0, 0.18),
+        0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.split-button__menu-item {
+    border: 0;
+    background: transparent;
+    color: inherit;
+    min-height: 48px;
+    padding: 0 12px;
+    text-align: left;
+    display: inline-flex;
+    align-items: center;
+    gap: 0.5rem;
+    font-size: var(--font-size-sm);
+    font-weight: 400;
+    line-height: 1.25;
+    cursor: pointer;
+    border-radius: 0;
+    transition:
+        background-color 120ms ease,
+        color 120ms ease;
+}
+
+.split-button__menu-item mat-icon {
+    color: inherit;
+    margin: 0;
+    width: 1rem;
+    height: 1rem;
+    font-size: 1rem;
+}
+
+.split-button__menu-item:hover {
+    background: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent);
+}
+
+.split-button__menu-item:focus-visible {
+    outline: none;
+    background: color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent);
+}
+
+.split-button__menu-item:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+
+.split-button--open .split-button__toggle mat-icon {
+    transform: rotate(180deg);
+    transition: transform 150ms ease;
+}
+
+.split-button--open .split-button__toggle {
+    border-radius: 999px;
+}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.ts
new file mode 100644
index 0000000000..bb3b6b5493
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/split-button/split-button.component.ts
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {
+    ConnectedPosition,
+    CdkConnectedOverlay,
+    CdkOverlayOrigin,
+} from '@angular/cdk/overlay';
+import {
+    Component,
+    EventEmitter,
+    Input,
+    Output,
+    ViewChild,
+} from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { MatIcon } from '@angular/material/icon';
+import { TranslatePipe } from '@ngx-translate/core';
+
+export interface SpSplitButtonAction {
+    label: string;
+    action: string;
+    icon?: string;
+    disabled?: boolean;
+}
+
+@Component({
+    selector: 'sp-split-button',
+    templateUrl: './split-button.component.html',
+    styleUrls: ['./split-button.component.scss'],
+    imports: [
+        CdkOverlayOrigin,
+        CdkConnectedOverlay,
+        MatButton,
+        MatIcon,
+        TranslatePipe,
+    ],
+})
+export class SpSplitButtonComponent {
+    @Input() label = '';
+    @Input() icon?: string;
+    @Input() actions: SpSplitButtonAction[] = [];
+    @Input() appearance: 'primary' | 'mat-basic' = 'primary';
+    @Input() disabled = false;
+    @Input() menuDisabled = false;
+    @Input() buttonType: 'button' | 'submit' | 'reset' = 'button';
+    @Input() menuAriaLabel = 'Additional actions';
+
+    @Output() primaryAction = new EventEmitter<void>();
+    @Output() actionSelected = new EventEmitter<SpSplitButtonAction>();
+
+    @ViewChild(CdkOverlayOrigin) overlayOrigin?: CdkOverlayOrigin;
+
+    menuOpen = false;
+    menuWidth = 0;
+
+    readonly overlayPositions: ConnectedPosition[] = [
+        {
+            originX: 'start',
+            originY: 'bottom',
+            overlayX: 'start',
+            overlayY: 'top',
+            offsetY: 0,
+        },
+        {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetY: 0,
+        },
+    ];
+
+    onPrimaryActionClicked(): void {
+        this.closeMenu();
+        this.primaryAction.emit();
+    }
+
+    onSplitActionClicked(action: SpSplitButtonAction): void {
+        this.closeMenu();
+        this.actionSelected.emit(action);
+    }
+
+    toggleMenu(event: MouseEvent): void {
+        event.stopPropagation();
+
+        if (this.isDropdownDisabled()) {
+            return;
+        }
+
+        this.menuWidth =
+            this.overlayOrigin?.elementRef.nativeElement.offsetWidth ?? 0;
+        this.menuOpen = !this.menuOpen;
+    }
+
+    closeMenu(): void {
+        this.menuOpen = false;
+    }
+
+    isDropdownDisabled(): boolean {
+        return this.disabled || this.menuDisabled || this.actions.length === 0;
+    }
+}
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts 
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index dc2cdd57b5..28efd371b9 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -39,6 +39,7 @@ export * from 
'./lib/components/date-input/date-input.component';
 export * from './lib/components/form-field/form-field.component';
 export * from './lib/components/form-label/form-label.component';
 export * from './lib/components/split-section/split-section.component';
+export * from './lib/components/split-button/split-button.component';
 export * from 
'./lib/components/sp-exception-message/sp-exception-message.component';
 export * from 
'./lib/components/sp-exception-message/exception-details-dialog/exception-details-dialog.component';
 export * from 
'./lib/components/sp-exception-message/exception-details/exception-details.component';

Reply via email to