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

tenthe pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 3e08e51f78 feat(#4718): add shared search select component (#4720)
3e08e51f78 is described below

commit 3e08e51f7878c263d43684d659989707f9c13375
Author: Philipp Zehnder <[email protected]>
AuthorDate: Mon Jul 20 09:32:13 2026 +0200

    feat(#4718): add shared search select component (#4720)
    
    Co-authored-by: Dominik Riemer <[email protected]>
---
 ui/STYLEGUIDE.md                                   |  62 +++++
 ui/deployment/i18n/de.json                         |   2 +
 ui/deployment/i18n/en.json                         |   2 +
 ui/deployment/i18n/pl.json                         |   2 +
 .../search-select-template.directive.ts            |  33 +++
 .../search-select/search-select.component.html     | 141 ++++++++++
 .../search-select/search-select.component.scss     | 104 +++++++
 .../search-select/search-select.component.ts       | 304 +++++++++++++++++++++
 .../object-manage-dialog.component.html            | 167 ++---------
 .../object-manage-dialog.component.ts              | 111 +-------
 .../object-permission-dialog.component.html        | 139 ++--------
 .../object-permission-dialog.component.ts          | 141 +---------
 .../streampipes/shared-ui/src/public-api.ts        |   2 +
 .../asset-details-labels.component.html            |  81 +-----
 .../asset-details-labels.component.ts              | 112 +-------
 .../chart-container/chart-container.component.ts   |  15 +-
 .../base/base-data-explorer-widget.directive.ts    |   7 +-
 .../models/dataview-dashboard.model.ts             |   2 +
 .../chart-data-settings.component.html             |  80 +-----
 .../data-settings/chart-data-settings.component.ts | 144 ++++++----
 .../adapter-deployment-settings.component.html     |  45 +--
 .../adapter-deployment-settings.component.ts       | 109 +-------
 22 files changed, 887 insertions(+), 918 deletions(-)

diff --git a/ui/STYLEGUIDE.md b/ui/STYLEGUIDE.md
index 59574173fb..2560fccaf0 100644
--- a/ui/STYLEGUIDE.md
+++ b/ui/STYLEGUIDE.md
@@ -102,6 +102,68 @@ To render smaller inputs in a dense layout, assign the CSS 
class `form-field-sma
 
 You can also define an optional tooltip which is shown above the label.
 
+#### Search Select
+
+Use `sp-search-select` when users need to select one or more items from a 
searchable list, such as datasets, labels, sites, asset types, users, roles or 
groups.
+
+Keep data loading, persistence and feature-specific actions outside the 
component. For example, actions such as `Manage Labels` or refresh buttons 
should remain in the parent view, usually in the surrounding `sp-form-field` 
actions.
+
+Single-select example:
+
+```html
+<sp-search-select
+  [items]="availableMeasurements"
+  [(value)]="selectedMeasurement"
+  [placeholder]="'Search datasets' | translate"
+>
+</sp-search-select>
+```
+
+Multi-select example:
+
+```html
+<sp-search-select
+  [items]="labels"
+  [(value)]="selectedLabels"
+  [multiple]="true"
+  [placeholder]="'Add labels' | translate"
+>
+</sp-search-select>
+```
+
+The component keeps the selected value as the full object. It displays items 
by convention using `label`, `name`, `measureName`, `title`, `email`, 
`groupName`, `value`, `filename`, `assetName`, `_id` or `id`, and filters 
client-side by the displayed value.
+
+Objects with a `color` property are rendered as small `sp-label` badges in 
both the selected value and the dropdown. This covers labels without adding 
label-specific inputs to the component:
+
+```html
+<sp-search-select
+  [items]="labels"
+  [(value)]="selectedLabels"
+  [multiple]="true"
+  [placeholder]="'Add labels' | translate"
+>
+</sp-search-select>
+```
+
+Use optional templates only when the default text or colored-label rendering 
is not enough:
+
+```html
+<sp-search-select
+  [items]="users"
+  [(value)]="selectedUsers"
+  [multiple]="true"
+  [placeholder]="'Add users' | translate"
+>
+  <ng-template spSearchSelectOption let-user>
+    {{ user.email }} ({{ user.principalId }})
+  </ng-template>
+
+  <ng-template spSearchSelectChip let-user> {{ user.email }} </ng-template>
+</sp-search-select>
+```
+
+Do not add feature-specific inputs such as label color keys, server-side 
search, action slots or custom comparison functions unless there is a concrete 
reusable need. Prefer the minimal API and keep feature behavior in the parent 
component.
+
 #### Label
 
 Use form labels to ensure a consistent layout of forms and labels.
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index f132229e81..726f1dbeff 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -73,6 +73,7 @@
   "Add node": "Knoten hinzufügen",
   "Add pipeline element": "Pipeline-Element hinzufügen",
   "Add role": "Rolle hinzufügen",
+  "Add service tag": "Service-Tag hinzufügen",
   "Add the current dashboard to an existing asset": "Das aktuelle Dashboard zu 
einem bestehenden Asset hinzufügen",
   "Add to Asset": "Zu Asset hinzufügen",
   "Add user": "Benutzer hinzufügen",
@@ -749,6 +750,7 @@
   "No more information": "Keine weiteren Informationen",
   "No nodes selected yet": "Noch keine Knoten ausgewählt",
   "No numeric or boolean fields available.": "Keine numerischen oder 
booleschen Felder verfügbar.",
+  "No results found": "Keine Ergebnisse gefunden",
   "No running adapters": "Keine laufenden Adapter",
   "No running pipeline elements": "Keine laufenden Pipeline-Elemente",
   "No table rows match the current filter.": "Keine Tabellenzeilen entsprechen 
dem aktuellen Filter.",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 675ee0f3a1..073f3b8872 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -73,6 +73,7 @@
   "Add node": null,
   "Add pipeline element": null,
   "Add role": null,
+  "Add service tag": null,
   "Add the current dashboard to an existing asset": null,
   "Add to Asset": null,
   "Add user": null,
@@ -749,6 +750,7 @@
   "No more information": null,
   "No nodes selected yet": null,
   "No numeric or boolean fields available.": null,
+  "No results found": null,
   "No running adapters": null,
   "No running pipeline elements": null,
   "No table rows match the current filter.": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index cc153b2023..0764b3d447 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -73,6 +73,7 @@
   "Add node": "Dodaj węzeł",
   "Add pipeline element": "Dodaj element strumienia",
   "Add role": "Dodaj rolę",
+  "Add service tag": "Dodaj tag usługi",
   "Add the current dashboard to an existing asset": "Dodaj bieżący pulpit do 
istniejącego zasobu",
   "Add to Asset": "Dodaj do zasobu",
   "Add user": "Dodaj użytkownika",
@@ -749,6 +750,7 @@
   "No more information": "Brak dalszych informacji",
   "No nodes selected yet": "Nie wybrano jeszcze żadnych węzłów",
   "No numeric or boolean fields available.": "Brak dostępnych pól liczbowych 
lub logicznych.",
+  "No results found": "Nie znaleziono wyników",
   "No running adapters": "Brak uruchomionych adapterów",
   "No running pipeline elements": "Brak uruchomionych elementów strumienia",
   "No table rows match the current filter.": "Żaden wiersz tabeli nie pasuje 
do bieżącego filtra.",
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select-template.directive.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select-template.directive.ts
new file mode 100644
index 0000000000..d3ed2f9a68
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select-template.directive.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { Directive, TemplateRef, inject } from '@angular/core';
+
+@Directive({
+    selector: 'ng-template[spSearchSelectOption]',
+})
+export class SearchSelectOptionTemplateDirective<T = unknown> {
+    readonly templateRef = inject<TemplateRef<{ $implicit: T }>>(TemplateRef);
+}
+
+@Directive({
+    selector: 'ng-template[spSearchSelectChip]',
+})
+export class SearchSelectChipTemplateDirective<T = unknown> {
+    readonly templateRef = inject<TemplateRef<{ $implicit: T }>>(TemplateRef);
+}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.html
new file mode 100644
index 0000000000..bdf1e11189
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.html
@@ -0,0 +1,141 @@
+<!--
+~ 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.
+~
+-->
+
+<mat-form-field appearance="outline" subscriptSizing="dynamic">
+    <mat-icon matPrefix>search</mat-icon>
+
+    @if (multiple()) {
+        <mat-chip-grid
+            #chipGrid
+            [attr.aria-label]="placeholderText()"
+            [disabled]="disabled()"
+        >
+            @for (item of selectedItems(); track trackItem($index, item)) {
+                @let color = itemColor(item);
+                @let contrastColor = itemContrastColor(item);
+                <mat-chip-row
+                    (removed)="removeItem(item)"
+                    [class.search-select-chip-colored]="color"
+                    [style.--sp-search-select-chip-bg]="color"
+                    [style.--sp-search-select-chip-fg]="contrastColor"
+                >
+                    @if (chipTemplate) {
+                        <ng-container
+                            [ngTemplateOutlet]="chipTemplate.templateRef"
+                            [ngTemplateOutletContext]="{ $implicit: item }"
+                        >
+                        </ng-container>
+                    } @else if (color) {
+                        <sp-label
+                            [labelText]="displayValue(item)"
+                            size="small"
+                            [color]="color"
+                        >
+                        </sp-label>
+                    } @else {
+                        {{ displayValue(item) }}
+                    }
+                    <button
+                        matChipRemove
+                        type="button"
+                        [style.color]="contrastColor"
+                    >
+                        <mat-icon [style.color]="contrastColor">
+                            cancel
+                        </mat-icon>
+                    </button>
+                </mat-chip-row>
+            }
+
+            <input
+                matInput
+                [placeholder]="placeholderText() | translate"
+                [value]="inputValue()"
+                (input)="onInput($any($event.target).value)"
+                [matAutocomplete]="searchSelectAutocomplete"
+                [matChipInputFor]="chipGrid"
+                [attr.data-cy]="dataCy()"
+                [disabled]="disabled()"
+            />
+        </mat-chip-grid>
+    } @else {
+        <input
+            matInput
+            [placeholder]="placeholderText() | translate"
+            [value]="inputValue()"
+            (input)="onInput($any($event.target).value)"
+            [matAutocomplete]="searchSelectAutocomplete"
+            [attr.data-cy]="dataCy()"
+            [disabled]="disabled()"
+        />
+    }
+
+    @if (selectedItems().length > 0 && !disabled()) {
+        <button
+            mat-icon-button
+            matSuffix
+            type="button"
+            [attr.aria-label]="'Clear search' | translate"
+            (click)="clearValue()"
+        >
+            <mat-icon>close</mat-icon>
+        </button>
+    }
+
+    <mat-autocomplete
+        #searchSelectAutocomplete="matAutocomplete"
+        autoActiveFirstOption
+        (opened)="onOpened()"
+        (closed)="onClosed()"
+        (optionSelected)="onSelected($event)"
+        [displayWith]="autocompleteDisplayWith"
+    >
+        @for (item of filteredItems(); track trackItem($index, item)) {
+            @let color = itemColor(item);
+            <mat-option [value]="item">
+                <mat-icon class="search-select-check-icon">
+                    @if (isSelected(item)) {
+                        check
+                    }
+                </mat-icon>
+                @if (optionTemplate) {
+                    <ng-container
+                        [ngTemplateOutlet]="optionTemplate.templateRef"
+                        [ngTemplateOutletContext]="{ $implicit: item }"
+                    >
+                    </ng-container>
+                } @else if (color) {
+                    <sp-label
+                        [labelText]="displayValue(item)"
+                        size="small"
+                        [color]="color"
+                    >
+                    </sp-label>
+                } @else {
+                    {{ displayValue(item) }}
+                }
+            </mat-option>
+        }
+
+        @if (filteredItems().length === 0) {
+            <mat-option disabled>
+                {{ 'No results found' | translate }}
+            </mat-option>
+        }
+    </mat-autocomplete>
+</mat-form-field>
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.scss
 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.scss
new file mode 100644
index 0000000000..bb0168afdd
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.scss
@@ -0,0 +1,104 @@
+/*!
+ * 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: block;
+    width: 100%;
+}
+
+mat-form-field {
+    width: 100%;
+}
+
+.search-select-check-icon {
+    margin-right: var(--space-xs);
+    width: 1.125rem;
+}
+
+.search-select-chip-colored {
+    background-color: transparent;
+    color: var(--sp-search-select-chip-fg);
+    min-height: auto;
+    padding: 0;
+    --mat-chip-container-height: auto;
+    --mat-chip-elevated-container-color: transparent;
+    --mat-chip-elevated-selected-container-color: transparent;
+    --mat-chip-flat-disabled-selected-container-color: transparent;
+    --mat-chip-outline-color: transparent;
+    --mat-chip-outline-width: 0;
+    --mat-chip-flat-selected-outline-width: 0;
+    --mat-chip-focus-outline-color: transparent;
+    --mat-chip-label-text-color: var(--sp-search-select-chip-fg);
+    --mat-chip-selected-label-text-color: var(--sp-search-select-chip-fg);
+    --mat-chip-with-trailing-icon-trailing-icon-color: var(
+        --sp-search-select-chip-fg
+    );
+    --mat-chip-selected-trailing-icon-color: var(--sp-search-select-chip-fg);
+    --mdc-chip-elevated-container-color: transparent;
+    --mdc-chip-outline-color: transparent;
+    --mdc-chip-outline-width: 0;
+    --mdc-chip-focus-outline-color: transparent;
+    --mdc-chip-label-text-color: var(--sp-search-select-chip-fg);
+    --mdc-chip-with-trailing-icon-trailing-icon-color: var(
+        --sp-search-select-chip-fg
+    );
+    --mdc-chip-with-trailing-icon-trailing-icon-hover-color: var(
+        --sp-search-select-chip-fg
+    );
+    --mdc-chip-with-trailing-icon-trailing-icon-focus-color: var(
+        --sp-search-select-chip-fg
+    );
+}
+
+// Angular Material renders chip outlines inside component internals. Colored
+// items use sp-label for the visible badge, so the Material wrapper stays 
flat.
+:host ::ng-deep .search-select-chip-colored {
+    .mat-mdc-chip-focus-overlay {
+        display: none;
+    }
+
+    .mdc-evolution-chip__cell,
+    .mdc-evolution-chip__action,
+    .mat-mdc-chip-action-label {
+        min-height: auto;
+    }
+
+    .mdc-evolution-chip__cell--primary,
+    .mdc-evolution-chip__action--primary {
+        padding-left: 0;
+        padding-right: 0;
+    }
+
+    .mdc-evolution-chip__action--primary::before {
+        border-color: transparent;
+        border-width: 0;
+    }
+
+    .mdc-evolution-chip__cell--trailing,
+    .mdc-evolution-chip__action--trailing {
+        padding-left: var(--space-xs);
+        padding-right: 0;
+    }
+
+    .mdc-evolution-chip__action,
+    .mdc-evolution-chip__text-label,
+    .mat-mdc-chip-action-label,
+    .mat-icon {
+        color: var(--sp-search-select-chip-fg);
+    }
+}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.ts
new file mode 100644
index 0000000000..abf032049b
--- /dev/null
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/components/search-select/search-select.component.ts
@@ -0,0 +1,304 @@
+/*
+ * 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 {
+    ChangeDetectionStrategy,
+    Component,
+    ContentChild,
+    ViewChild,
+    booleanAttribute,
+    computed,
+    inject,
+    input,
+    model,
+    signal,
+} from '@angular/core';
+import { NgTemplateOutlet } from '@angular/common';
+import {
+    MatAutocomplete,
+    MatAutocompleteSelectedEvent,
+    MatAutocompleteTrigger,
+} from '@angular/material/autocomplete';
+import {
+    MatChipGrid,
+    MatChipRow,
+    MatChipsModule,
+} from '@angular/material/chips';
+import {
+    MatFormField,
+    MatPrefix,
+    MatSuffix,
+} from '@angular/material/form-field';
+import { MatIcon } from '@angular/material/icon';
+import { MatIconButton } from '@angular/material/button';
+import { MatInput } from '@angular/material/input';
+import { MatOption } from '@angular/material/core';
+import { TranslatePipe } from '@ngx-translate/core';
+import {
+    SearchSelectChipTemplateDirective,
+    SearchSelectOptionTemplateDirective,
+} from './search-select-template.directive';
+import { SpColorizationService } from '../../services/colorization.service';
+import { SpLabelComponent } from '../sp-label/sp-label.component';
+
+const DISPLAY_VALUE_FIELDS = [
+    'label',
+    'name',
+    'measureName',
+    'title',
+    'email',
+    'groupName',
+    'value',
+    'filename',
+    'assetName',
+    '_id',
+    'id',
+];
+
+const IDENTITY_FIELDS = [
+    '_id',
+    'id',
+    'measureName',
+    'principalId',
+    'groupId',
+    'value',
+    'filename',
+    'assetName',
+];
+
+@Component({
+    selector: 'sp-search-select',
+    templateUrl: './search-select.component.html',
+    styleUrls: ['./search-select.component.scss'],
+    imports: [
+        MatAutocomplete,
+        MatAutocompleteTrigger,
+        MatChipGrid,
+        MatChipRow,
+        MatChipsModule,
+        MatFormField,
+        MatIcon,
+        MatIconButton,
+        MatInput,
+        MatOption,
+        MatPrefix,
+        MatSuffix,
+        NgTemplateOutlet,
+        SpLabelComponent,
+        TranslatePipe,
+    ],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SearchSelectComponent<T = unknown> {
+    private colorizationService = inject(SpColorizationService);
+
+    readonly items = input<T[]>([]);
+    readonly multiple = input(false);
+    readonly disabled = input(false, { transform: booleanAttribute });
+    readonly placeholder = input<string | undefined>();
+    readonly dataCy = input<string | undefined>();
+
+    readonly value = model<T | T[] | undefined>(undefined);
+
+    readonly searchText = signal('');
+    readonly panelOpen = signal(false);
+
+    @ContentChild(SearchSelectOptionTemplateDirective)
+    optionTemplate?: SearchSelectOptionTemplateDirective<T>;
+
+    @ContentChild(SearchSelectChipTemplateDirective)
+    chipTemplate?: SearchSelectChipTemplateDirective<T>;
+
+    @ViewChild(MatAutocompleteTrigger)
+    autocompleteTrigger?: MatAutocompleteTrigger;
+
+    readonly selectedItems = computed(() => {
+        const value = this.value();
+
+        if (!this.multiple()) {
+            return value && !Array.isArray(value) ? [value] : [];
+        }
+
+        return Array.isArray(value) ? value : [];
+    });
+
+    readonly selectedValue = computed(() => this.selectedItems()[0]);
+
+    readonly selectedText = computed(() => {
+        const selected = this.selectedValue();
+        return selected ? this.displayValue(selected) : '';
+    });
+
+    readonly inputValue = computed(() =>
+        this.panelOpen() || this.multiple()
+            ? this.searchText()
+            : this.selectedText(),
+    );
+
+    readonly placeholderText = computed(() => {
+        if (this.placeholder()) {
+            return this.placeholder();
+        }
+
+        return this.multiple() ? 'Add' : 'Select';
+    });
+
+    readonly filteredItems = computed(() => {
+        const query = this.searchText().trim().toLowerCase();
+
+        if (!query) {
+            return this.items();
+        }
+
+        return this.items().filter(item =>
+            this.displayValue(item).toLowerCase().includes(query),
+        );
+    });
+
+    readonly autocompleteDisplayWith = (item: T): string =>
+        this.displayValue(item);
+
+    onInput(value: string): void {
+        this.searchText.set(value);
+    }
+
+    onOpened(): void {
+        this.panelOpen.set(true);
+        this.searchText.set('');
+    }
+
+    onClosed(): void {
+        this.panelOpen.set(false);
+        this.searchText.set('');
+    }
+
+    onSelected(event: MatAutocompleteSelectedEvent): void {
+        const item = event.option.value as T;
+
+        if (this.multiple()) {
+            this.toggleItem(item);
+            setTimeout(() => this.autocompleteTrigger?.openPanel());
+        } else {
+            this.value.set(item);
+        }
+
+        this.searchText.set('');
+    }
+
+    removeItem(item: T): void {
+        if (!this.multiple()) {
+            if (this.isSelected(item)) {
+                this.value.set(undefined);
+            }
+            return;
+        }
+
+        this.value.set(
+            this.selectedItems().filter(
+                selected => !this.compare(selected, item),
+            ),
+        );
+    }
+
+    clearValue(): void {
+        this.value.set(this.multiple() ? [] : undefined);
+        this.searchText.set('');
+    }
+
+    isSelected(item: T): boolean {
+        return this.selectedItems().some(selected =>
+            this.compare(selected, item),
+        );
+    }
+
+    displayValue(item: T): string {
+        if (item === undefined || item === null) {
+            return '';
+        }
+
+        if (
+            typeof item === 'string' ||
+            typeof item === 'number' ||
+            typeof item === 'boolean'
+        ) {
+            return String(item);
+        }
+
+        const record = this.toRecord(item);
+        const displayValue = DISPLAY_VALUE_FIELDS.map(
+            field => record?.[field],
+        ).find(value => value !== undefined && value !== null);
+
+        return displayValue ? String(displayValue) : '';
+    }
+
+    itemColor(item: T): string | undefined {
+        const color = this.toRecord(item)?.['color'];
+        return typeof color === 'string' ? color : undefined;
+    }
+
+    itemContrastColor(item: T): string | undefined {
+        const color = this.itemColor(item);
+        return color
+            ? this.colorizationService.generateContrastColor(color)
+            : undefined;
+    }
+
+    protected trackItem = (_index: number, item: T): string =>
+        this.itemKey(item);
+
+    private toggleItem(item: T): void {
+        if (this.isSelected(item)) {
+            this.removeItem(item);
+        } else {
+            this.value.set([...this.selectedItems(), item]);
+        }
+    }
+
+    private compare(a: T, b: T): boolean {
+        if (a === b) {
+            return true;
+        }
+
+        const aRecord = this.toRecord(a);
+        const bRecord = this.toRecord(b);
+
+        return IDENTITY_FIELDS.some(key => {
+            const aValue = aRecord?.[key];
+            const bValue = bRecord?.[key];
+            return aValue !== undefined && aValue === bValue;
+        });
+    }
+
+    private itemKey(item: T): string {
+        const record = this.toRecord(item);
+        const key = IDENTITY_FIELDS.map(field => record?.[field]).find(
+            value => value !== undefined && value !== null,
+        );
+
+        return String(key ?? this.displayValue(item));
+    }
+
+    private toRecord(item: T): Record<string, unknown> | undefined {
+        if (item === undefined || item === null || typeof item !== 'object') {
+            return undefined;
+        }
+
+        return item as Record<string, unknown>;
+    }
+}
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html
index e8ceedcf8b..ae3483a221 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html
@@ -156,77 +156,21 @@
                                         [level]="3"
                                         [label]="'Authorized Users' | 
translate"
                                     >
-                                        <mat-form-field
-                                            color="accent"
-                                            data-cy="authorized-users-label"
-                                        >
-                                            <mat-chip-grid
-                                                #chipList
-                                                [attr.aria-label]="
-                                                    'User selection' | 
translate
+                                        <div data-cy="authorized-users-label">
+                                            <sp-search-select
+                                                [items]="availableUsers"
+                                                
[value]="grantedUserAuthorities"
+                                                [multiple]="true"
+                                                [placeholder]="
+                                                    'Add' | translate
                                                 "
-                                            >
-                                                @for (
-                                                    user of 
grantedUserAuthorities;
-                                                    track user
-                                                ) {
-                                                    <mat-chip-row
-                                                        selectable="true"
-                                                        removable="true"
-                                                        (removed)="
-                                                            removeUser(user)
-                                                        "
-                                                    >
-                                                        {{ user.email }}
-                                                        <button matChipRemove>
-                                                            <mat-icon
-                                                                
>cancel</mat-icon
-                                                            >
-                                                        </button>
-                                                    </mat-chip-row>
-                                                }
-                                                <input
-                                                    matInput
-                                                    [placeholder]="
-                                                        'Add' | translate
-                                                    "
-                                                    #userInput
-                                                    [formControl]="userCtrl"
-                                                    
[matAutocomplete]="userAuto"
-                                                    
[matChipInputFor]="chipList"
-                                                    
[matChipInputSeparatorKeyCodes]="
-                                                        separatorKeysCodes
-                                                    "
-                                                    data-cy="authorized-user"
-                                                    (matChipInputTokenEnd)="
-                                                        addUser($event)
-                                                    "
-                                                />
-                                            </mat-chip-grid>
-
-                                            <mat-autocomplete
-                                                #userAuto="matAutocomplete"
-                                                (optionSelected)="
-                                                    userSelected($event)
+                                                dataCy="authorized-user"
+                                                (valueChange)="
+                                                    
onGrantedUsersChange($event)
                                                 "
                                             >
-                                                @for (
-                                                    user of filteredUsers$
-                                                        | async;
-                                                    track user
-                                                ) {
-                                                    <mat-option
-                                                        [value]="user"
-                                                        [attr.data-cy]="
-                                                            'user-option-' +
-                                                            user.email
-                                                        "
-                                                    >
-                                                        {{ user.email }}
-                                                    </mat-option>
-                                                }
-                                            </mat-autocomplete>
-                                        </mat-form-field>
+                                            </sp-search-select>
+                                        </div>
                                     </sp-form-field>
                                 </div>
 
@@ -237,80 +181,25 @@
                                             'Authorized Groups' | translate
                                         "
                                     >
-                                        <mat-form-field
-                                            color="accent"
-                                            data-cy="authorized-groups-label"
-                                        >
-                                            <mat-chip-grid
-                                                #chipList
-                                                [attr.aria-label]="
-                                                    'Group selection'
-                                                        | translate
+                                        <div data-cy="authorized-groups-label">
+                                            <sp-search-select
+                                                [items]="availableGroups"
+                                                [value]="
+                                                    grantedGroupAuthorities
                                                 "
-                                            >
-                                                @for (
-                                                    group of 
grantedGroupAuthorities;
-                                                    track group
-                                                ) {
-                                                    <mat-chip-row
-                                                        selectable="true"
-                                                        removable="true"
-                                                        (removed)="
-                                                            removeGroup(group)
-                                                        "
-                                                    >
-                                                        {{ group.groupName }}
-                                                        <button matChipRemove>
-                                                            <mat-icon
-                                                                
>cancel</mat-icon
-                                                            >
-                                                        </button>
-                                                    </mat-chip-row>
-                                                }
-                                                <input
-                                                    matInput
-                                                    [placeholder]="
-                                                        'Add' | translate
-                                                    "
-                                                    data-cy="authorized-group"
-                                                    #groupInput
-                                                    [formControl]="groupCtrl"
-                                                    [matAutocomplete]="
-                                                        groupAuto
-                                                    "
-                                                    
[matChipInputFor]="chipList"
-                                                    
[matChipInputSeparatorKeyCodes]="
-                                                        separatorKeysCodes
-                                                    "
-                                                    (matChipInputTokenEnd)="
-                                                        addGroup($event)
-                                                    "
-                                                />
-                                            </mat-chip-grid>
-
-                                            <mat-autocomplete
-                                                #groupAuto="matAutocomplete"
-                                                (optionSelected)="
-                                                    groupSelected($event)
+                                                [multiple]="true"
+                                                [placeholder]="
+                                                    'Add' | translate
+                                                "
+                                                dataCy="authorized-group"
+                                                (valueChange)="
+                                                    onGrantedGroupsChange(
+                                                        $event
+                                                    )
                                                 "
                                             >
-                                                @for (
-                                                    group of filteredGroups$
-                                                        | async;
-                                                    track group
-                                                ) {
-                                                    <mat-option
-                                                        [attr.data-cy]="
-                                                            'group-option-' +
-                                                            group.groupName
-                                                        "
-                                                        [value]="group"
-                                                    >
-                                                        {{ group.groupName }}
-                                                    </mat-option>
-                                                }
-                                            </mat-autocomplete>
-                                        </mat-form-field>
+                                            </sp-search-select>
+                                        </div>
                                     </sp-form-field>
                                 </div>
                             }
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts
index e87d0d1c9b..37d3e09722 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts
@@ -16,11 +16,9 @@
  *
  */
 
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
 import {
     AfterViewInit,
     Component,
-    ElementRef,
     inject,
     Input,
     OnDestroy,
@@ -54,27 +52,7 @@ import {
     UserService,
     UserInfo,
 } from '@streampipes/platform-services';
-import {
-    MatChipGrid,
-    MatChipInput,
-    MatChipInputEvent,
-    MatChipRemove,
-    MatChipRow,
-} from '@angular/material/chips';
-import {
-    combineLatest,
-    firstValueFrom,
-    isObservable,
-    Observable,
-    shareReplay,
-    zip,
-} from 'rxjs';
-import {
-    MatAutocomplete,
-    MatAutocompleteSelectedEvent,
-    MatAutocompleteTrigger,
-} from '@angular/material/autocomplete';
-import { map, startWith } from 'rxjs/operators';
+import { firstValueFrom, isObservable, zip } from 'rxjs';
 import {
     FlexDirective,
     LayoutAlignDirective,
@@ -91,7 +69,6 @@ import { MatButton, MatIconButton } from 
'@angular/material/button';
 import { MatTooltip } from '@angular/material/tooltip';
 import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
 import { MatDivider } from '@angular/material/divider';
-import { AsyncPipe } from '@angular/common';
 import { TranslatePipe } from '@ngx-translate/core';
 import {
     ObjectManageDialogResource,
@@ -99,6 +76,7 @@ import {
     ObjectManageDialogResult,
     ObjectManageDialogSaveMode,
 } from './model/object-manage-model.model';
+import { SearchSelectComponent } from 
'../../components/search-select/search-select.component';
 
 @Component({
     selector: 'sp-object-manage-dialog',
@@ -117,23 +95,17 @@ import {
         MatSelect,
         MatOption,
         MatCheckbox,
-        MatChipGrid,
-        MatChipRow,
-        MatChipRemove,
         MatIcon,
         MatInput,
-        MatAutocompleteTrigger,
-        MatChipInput,
-        MatAutocomplete,
         LayoutGapDirective,
         MatIconButton,
         MatTooltip,
         CdkCopyToClipboard,
         MatDivider,
         MatButton,
-        AsyncPipe,
         TranslatePipe,
         AssetLinkConfigurationComponent,
+        SearchSelectComponent,
     ],
 })
 export class ObjectManageDialogComponent<
@@ -141,8 +113,6 @@ export class ObjectManageDialogComponent<
 >
     implements OnInit, AfterViewInit, OnDestroy
 {
-    separatorKeysCodes: number[] = [ENTER, COMMA];
-
     @Input()
     createMode: boolean = false;
 
@@ -197,19 +167,12 @@ export class ObjectManageDialogComponent<
     allUsers: ShortUserInfo[];
     allGroups: Group[];
 
-    filteredUsers$: Observable<ShortUserInfo[]>;
-    filteredGroups$: Observable<Group[]>;
-
     loading = true;
     permissionDenied = false;
     private assetRestoreInterval?: ReturnType<typeof setInterval>;
 
-    @ViewChild('userInput') userInput: ElementRef<HTMLInputElement>;
-    @ViewChild('groupInput') groupInput: ElementRef<HTMLInputElement>;
     @ViewChild(AssetLinkConfigurationComponent)
     assetLinkConfigurationComponent?: AssetLinkConfigurationComponent;
-    userCtrl = new UntypedFormControl();
-    groupCtrl = new UntypedFormControl();
 
     private fb = inject(UntypedFormBuilder);
     private dialogRef = inject(
@@ -363,34 +326,6 @@ export class ObjectManageDialogComponent<
                     new UntypedFormControl(this.permission.readAnonymous),
                 );
             }
-            this.filteredUsers$ = combineLatest([
-                this.userCtrl.valueChanges.pipe(startWith(null)),
-                this.parentForm
-                    .get('owner')!
-                    .valueChanges.pipe(
-                        startWith(this.parentForm.get('owner')!.value),
-                    ),
-            ]).pipe(
-                map(([username]) => {
-                    const base = this.allUsers.filter(
-                        u => !this.isOwnerOrAdded(u),
-                    );
-                    return username ? this._filter(username) : base.slice();
-                }),
-                shareReplay({ bufferSize: 1, refCount: true }),
-            );
-
-            this.filteredGroups$ = this.groupCtrl.valueChanges.pipe(
-                startWith(null),
-                map((groupName: string | null) => {
-                    return groupName
-                        ? this._filterGroup(groupName)
-                        : this.allGroups
-                              .filter(g => !this.isGroupAdded(g))
-                              .slice();
-                }),
-            );
-
             this.permission.grantedAuthorities.forEach(authority => {
                 if (authority.principalType === 'GROUP') {
                     this.addGroupToSelection(authority);
@@ -471,42 +406,22 @@ export class ObjectManageDialogComponent<
         this.dialogRef.close(result);
     }
 
-    removeUser(user: ShortUserInfo) {
-        const currentIndex = this.grantedUserAuthorities.findIndex(
-            u => u.principalId === user.principalId,
-        );
-        this.grantedUserAuthorities.splice(currentIndex, 1);
-        this.userCtrl.setValue(null);
-    }
-
-    removeGroup(group: Group) {
-        const currentIndex = this.grantedGroupAuthorities.findIndex(
-            u => u.groupId === group.groupId,
-        );
-        this.grantedGroupAuthorities.splice(currentIndex, 1);
-        this.groupCtrl.setValue(null);
-    }
-
-    addUser(event: MatChipInputEvent) {
-        event.chipInput.clear();
-        this.userCtrl.setValue(null);
+    get availableUsers(): ShortUserInfo[] {
+        return this.allUsers?.filter(u => !this.isOwnerOrAdded(u)) ?? [];
     }
 
-    addGroup(event: MatChipInputEvent) {
-        event.chipInput.clear();
-        this.groupCtrl.setValue(null);
+    get availableGroups(): Group[] {
+        return this.allGroups?.filter(g => !this.isGroupAdded(g)) ?? [];
     }
 
-    userSelected(event: MatAutocompleteSelectedEvent) {
-        this.grantedUserAuthorities.push(event.option.value);
-        this.userInput.nativeElement.value = '';
-        this.userCtrl.setValue(null);
+    onGrantedUsersChange(
+        users: ShortUserInfo | ShortUserInfo[] | undefined,
+    ): void {
+        this.grantedUserAuthorities = Array.isArray(users) ? users : [];
     }
 
-    groupSelected(event: MatAutocompleteSelectedEvent) {
-        this.grantedGroupAuthorities.push(event.option.value);
-        this.groupInput.nativeElement.value = '';
-        this.groupCtrl.setValue(null);
+    onGrantedGroupsChange(groups: Group | Group[] | undefined): void {
+        this.grantedGroupAuthorities = Array.isArray(groups) ? groups : [];
     }
 
     private addUserToSelection(authority: PermissionEntry) {
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.html
 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.html
index d6bfae790c..db71f21d62 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.html
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.html
@@ -74,66 +74,19 @@
                                 [level]="3"
                                 [label]="'Authorized Users' | translate"
                             >
-                                <mat-form-field
-                                    color="accent"
-                                    data-cy="authorized-users-label"
-                                >
-                                    <mat-chip-grid
-                                        #chipList
-                                        [attr.aria-label]="
-                                            'User selection' | translate
+                                <div data-cy="authorized-users-label">
+                                    <sp-search-select
+                                        [items]="availableUsers"
+                                        [value]="grantedUserAuthorities"
+                                        [multiple]="true"
+                                        [placeholder]="'Add' | translate"
+                                        dataCy="authorized-user"
+                                        (valueChange)="
+                                            onGrantedUsersChange($event)
                                         "
                                     >
-                                        @for (
-                                            user of grantedUserAuthorities;
-                                            track user
-                                        ) {
-                                            <mat-chip-row
-                                                selectable="true"
-                                                removable="true"
-                                                (removed)="removeUser(user)"
-                                            >
-                                                {{ user.email }}
-                                                <button matChipRemove>
-                                                    <mat-icon>cancel</mat-icon>
-                                                </button>
-                                            </mat-chip-row>
-                                        }
-                                        <input
-                                            matInput
-                                            [placeholder]="'Add' | translate"
-                                            #userInput
-                                            [formControl]="userCtrl"
-                                            [matAutocomplete]="userAuto"
-                                            [matChipInputFor]="chipList"
-                                            [matChipInputSeparatorKeyCodes]="
-                                                separatorKeysCodes
-                                            "
-                                            data-cy="authorized-user"
-                                            (matChipInputTokenEnd)="
-                                                addUser($event)
-                                            "
-                                        />
-                                    </mat-chip-grid>
-                                    <mat-autocomplete
-                                        #userAuto="matAutocomplete"
-                                        (optionSelected)="userSelected($event)"
-                                    >
-                                        @for (
-                                            user of filteredUsers$ | async;
-                                            track user
-                                        ) {
-                                            <mat-option
-                                                [value]="user"
-                                                [attr.data-cy]="
-                                                    'user-option-' + user.email
-                                                "
-                                            >
-                                                {{ user.email }}
-                                            </mat-option>
-                                        }
-                                    </mat-autocomplete>
-                                </mat-form-field>
+                                    </sp-search-select>
+                                </div>
                             </sp-form-field>
                         </sp-split-section>
                     }
@@ -146,67 +99,19 @@
                                 [level]="3"
                                 [label]="'Authorized Groups' | translate"
                             >
-                                <mat-form-field
-                                    color="accent"
-                                    data-cy="authorized-groups-label"
-                                >
-                                    <mat-chip-grid
-                                        #chipList
-                                        [attr.aria-label]="
-                                            'Group selection' | translate
+                                <div data-cy="authorized-groups-label">
+                                    <sp-search-select
+                                        [items]="availableGroups"
+                                        [value]="grantedGroupAuthorities"
+                                        [multiple]="true"
+                                        [placeholder]="'Add' | translate"
+                                        dataCy="authorized-group"
+                                        (valueChange)="
+                                            onGrantedGroupsChange($event)
                                         "
                                     >
-                                        @for (
-                                            group of grantedGroupAuthorities;
-                                            track group
-                                        ) {
-                                            <mat-chip-row
-                                                selectable="true"
-                                                removable="true"
-                                                (removed)="removeGroup(group)"
-                                            >
-                                                {{ group.groupName }}
-                                                <button matChipRemove>
-                                                    <mat-icon>cancel</mat-icon>
-                                                </button>
-                                            </mat-chip-row>
-                                        }
-                                        <input
-                                            matInput
-                                            [placeholder]="'Add' | translate"
-                                            data-cy="authorized-group"
-                                            #groupInput
-                                            [formControl]="groupCtrl"
-                                            [matAutocomplete]="groupAuto"
-                                            [matChipInputFor]="chipList"
-                                            [matChipInputSeparatorKeyCodes]="
-                                                separatorKeysCodes
-                                            "
-                                            (matChipInputTokenEnd)="
-                                                addGroup($event)
-                                            "
-                                        />
-                                    </mat-chip-grid>
-                                    <mat-autocomplete
-                                        #groupAuto="matAutocomplete"
-                                        
(optionSelected)="groupSelected($event)"
-                                    >
-                                        @for (
-                                            group of filteredGroups$ | async;
-                                            track group
-                                        ) {
-                                            <mat-option
-                                                [attr.data-cy]="
-                                                    'group-option-' +
-                                                    group.groupName
-                                                "
-                                                [value]="group"
-                                            >
-                                                {{ group.groupName }}
-                                            </mat-option>
-                                        }
-                                    </mat-autocomplete>
-                                </mat-form-field>
+                                    </sp-search-select>
+                                </div>
                             </sp-form-field>
                         </sp-split-section>
                     }
diff --git 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.ts
 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.ts
index 984205efaa..119a9d83d9 100644
--- 
a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.ts
+++ 
b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-permission-dialog/object-permission-dialog.component.ts
@@ -16,15 +16,7 @@
  *
  */
 
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
-import {
-    Component,
-    ElementRef,
-    inject,
-    Input,
-    OnInit,
-    ViewChild,
-} from '@angular/core';
+import { Component, inject, Input, OnInit } from '@angular/core';
 import { DialogRef } from '../base-dialog/dialog-ref';
 import {
     FormsModule,
@@ -44,20 +36,7 @@ import {
     UserGroupService,
     UserService,
 } from '@streampipes/platform-services';
-import {
-    MatChipGrid,
-    MatChipInput,
-    MatChipInputEvent,
-    MatChipRemove,
-    MatChipRow,
-} from '@angular/material/chips';
-import { combineLatest, Observable, shareReplay, zip } from 'rxjs';
-import {
-    MatAutocomplete,
-    MatAutocompleteSelectedEvent,
-    MatAutocompleteTrigger,
-} from '@angular/material/autocomplete';
-import { map, startWith } from 'rxjs/operators';
+import { zip } from 'rxjs';
 import {
     FlexDirective,
     LayoutAlignDirective,
@@ -71,13 +50,12 @@ import { MatFormField } from '@angular/material/form-field';
 import { MatOption, MatSelect } from '@angular/material/select';
 import { MatCheckbox } from '@angular/material/checkbox';
 import { MatIcon } from '@angular/material/icon';
-import { MatInput } from '@angular/material/input';
 import { MatButton, MatIconButton } from '@angular/material/button';
 import { MatTooltip } from '@angular/material/tooltip';
 import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
 import { MatDivider } from '@angular/material/divider';
-import { AsyncPipe } from '@angular/common';
 import { TranslatePipe } from '@ngx-translate/core';
+import { SearchSelectComponent } from 
'../../components/search-select/search-select.component';
 
 @Component({
     selector: 'sp-object-permission-dialog',
@@ -96,27 +74,18 @@ import { TranslatePipe } from '@ngx-translate/core';
         MatSelect,
         MatOption,
         MatCheckbox,
-        MatChipGrid,
-        MatChipRow,
-        MatChipRemove,
         MatIcon,
-        MatInput,
-        MatAutocompleteTrigger,
-        MatChipInput,
-        MatAutocomplete,
         LayoutGapDirective,
         MatIconButton,
         MatTooltip,
         CdkCopyToClipboard,
         MatDivider,
         MatButton,
-        AsyncPipe,
         TranslatePipe,
+        SearchSelectComponent,
     ],
 })
 export class ObjectPermissionDialogComponent implements OnInit {
-    separatorKeysCodes: number[] = [ENTER, COMMA];
-
     @Input()
     objectInstanceId: string;
 
@@ -140,17 +109,9 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
     allUsers: ShortUserInfo[];
     allGroups: Group[];
 
-    filteredUsers$: Observable<ShortUserInfo[]>;
-    filteredGroups$: Observable<Group[]>;
-
     loading = true;
     permissionDenied = false;
 
-    @ViewChild('userInput') userInput: ElementRef<HTMLInputElement>;
-    @ViewChild('groupInput') groupInput: ElementRef<HTMLInputElement>;
-    userCtrl = new UntypedFormControl();
-    groupCtrl = new UntypedFormControl();
-
     private fb = inject(UntypedFormBuilder);
     private dialogRef = inject(DialogRef<ObjectPermissionDialogComponent>);
     private permissionsService = inject(PermissionsService);
@@ -208,34 +169,6 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
                     new UntypedFormControl(this.permission.readAnonymous),
                 );
             }
-            this.filteredUsers$ = combineLatest([
-                this.userCtrl.valueChanges.pipe(startWith(null)),
-                this.parentForm
-                    .get('owner')!
-                    .valueChanges.pipe(
-                        startWith(this.parentForm.get('owner')!.value),
-                    ),
-            ]).pipe(
-                map(([username]) => {
-                    const base = this.allUsers.filter(
-                        u => !this.isOwnerOrAdded(u),
-                    );
-                    return username ? this._filter(username) : base.slice();
-                }),
-                shareReplay({ bufferSize: 1, refCount: true }),
-            );
-
-            this.filteredGroups$ = this.groupCtrl.valueChanges.pipe(
-                startWith(null),
-                map((groupName: string | null) => {
-                    return groupName
-                        ? this._filterGroup(groupName)
-                        : this.allGroups
-                              .filter(g => !this.isGroupAdded(g))
-                              .slice();
-                }),
-            );
-
             this.permission.grantedAuthorities.forEach(authority => {
                 if (authority.principalType === 'GROUP') {
                     this.addGroupToSelection(authority);
@@ -285,42 +218,22 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
         this.dialogRef.close(refresh);
     }
 
-    removeUser(user: ShortUserInfo) {
-        const currentIndex = this.grantedUserAuthorities.findIndex(
-            u => u.principalId === user.principalId,
-        );
-        this.grantedUserAuthorities.splice(currentIndex, 1);
-        this.userCtrl.setValue(null);
-    }
-
-    removeGroup(group: Group) {
-        const currentIndex = this.grantedGroupAuthorities.findIndex(
-            u => u.groupId === group.groupId,
-        );
-        this.grantedGroupAuthorities.splice(currentIndex, 1);
-        this.groupCtrl.setValue(null);
-    }
-
-    addUser(event: MatChipInputEvent) {
-        event.chipInput.clear();
-        this.userCtrl.setValue(null);
+    get availableUsers(): ShortUserInfo[] {
+        return this.allUsers?.filter(u => !this.isOwnerOrAdded(u)) ?? [];
     }
 
-    addGroup(event: MatChipInputEvent) {
-        event.chipInput.clear();
-        this.groupCtrl.setValue(null);
+    get availableGroups(): Group[] {
+        return this.allGroups?.filter(g => !this.isGroupAdded(g)) ?? [];
     }
 
-    userSelected(event: MatAutocompleteSelectedEvent) {
-        this.grantedUserAuthorities.push(event.option.value);
-        this.userInput.nativeElement.value = '';
-        this.userCtrl.setValue(null);
+    onGrantedUsersChange(
+        users: ShortUserInfo | ShortUserInfo[] | undefined,
+    ): void {
+        this.grantedUserAuthorities = Array.isArray(users) ? users : [];
     }
 
-    groupSelected(event: MatAutocompleteSelectedEvent) {
-        this.grantedGroupAuthorities.push(event.option.value);
-        this.groupInput.nativeElement.value = '';
-        this.groupCtrl.setValue(null);
+    onGrantedGroupsChange(groups: Group | Group[] | undefined): void {
+        this.grantedGroupAuthorities = Array.isArray(groups) ? groups : [];
     }
 
     private addUserToSelection(authority: PermissionEntry) {
@@ -335,32 +248,6 @@ export class ObjectPermissionDialogComponent implements 
OnInit {
         this.grantedGroupAuthorities.push(group);
     }
 
-    private _filter(value: any): ShortUserInfo[] {
-        const isUserAccount = value instanceof ShortUserInfo;
-        const filterValue = isUserAccount
-            ? value.email.toLowerCase()
-            : value.toLowerCase();
-        return this.allUsers.filter(u => {
-            return (
-                u.email.toLowerCase().startsWith(filterValue) &&
-                !this.isOwnerOrAdded(u)
-            );
-        });
-    }
-
-    private _filterGroup(value: any): Group[] {
-        const isGroup = value instanceof Group;
-        const filterValue = isGroup
-            ? value.groupName.toLowerCase()
-            : value.toLowerCase();
-        return this.allGroups.filter(g => {
-            return (
-                g.groupName.toLowerCase().startsWith(filterValue) &&
-                !this.isGroupAdded(g)
-            );
-        });
-    }
-
     private isOwnerOrAdded(user: ShortUserInfo): boolean {
         return (
             this.parentForm.get('owner').getRawValue() === user.principalId ||
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts 
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index e586495e2c..dfa1358113 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -46,6 +46,8 @@ export * from 
'./lib/components/element-id/element-id.component';
 export * from './lib/components/form-field/form-field.component';
 export * from './lib/components/form-label/form-label.component';
 export * from 
'./lib/components/property-scope-badge/property-scope-badge.component';
+export * from './lib/components/search-select/search-select.component';
+export * from 
'./lib/components/search-select/search-select-template.directive';
 export * from './lib/components/progress-bar/progress-bar.component';
 export * from './lib/components/split-section/split-section.component';
 export * from './lib/components/split-button/split-button.component';
diff --git 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.html
 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.html
index a8b8334d86..b8c81222a0 100644
--- 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.html
+++ 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.html
@@ -44,78 +44,21 @@
                                 <mat-icon>refresh</mat-icon>
                             </button>
                         </div>
-                        <mat-form-field
-                            class="asset-label-chip-field"
-                            [class.asset-label-chip-field-disabled]="
+                        <sp-search-select
+                            [items]="allLabels"
+                            [value]="labels"
+                            [multiple]="true"
+                            [disabled]="allLabels.length === 0"
+                            [placeholder]="
                                 allLabels.length === 0
+                                    ? ('No labels available - Click \'Manage 
Labels\''
+                                      | translate)
+                                    : ('Click to add label' | translate)
                             "
+                            dataCy="asset-label-input"
+                            (valueChange)="onLabelsChange($event)"
                         >
-                            <mat-chip-grid
-                                #chipGrid
-                                aria-label="Labels"
-                                [disabled]="allLabels.length === 0"
-                            >
-                                @for (label of labels; track label) {
-                                    <mat-chip-row
-                                        (removed)="remove(label)"
-                                        [ngStyle]="{ background: label.color }"
-                                    >
-                                        <span
-                                            [ngStyle]="{
-                                                color: labelTextColors[
-                                                    label._id
-                                                ],
-                                            }"
-                                            >{{ label.label }}</span
-                                        >
-                                        <button
-                                            matChipRemove
-                                            [ngStyle]="{
-                                                color: labelTextColors[
-                                                    label._id
-                                                ],
-                                            }"
-                                        >
-                                            <mat-icon>cancel</mat-icon>
-                                        </button>
-                                    </mat-chip-row>
-                                }
-                                <input
-                                    [placeholder]="
-                                        allLabels.length === 0
-                                            ? ('No labels available - Click 
\'Manage Labels\''
-                                              | translate)
-                                            : ('Click to add label' | 
translate)
-                                    "
-                                    data-cy="asset-label-input"
-                                    class="asset-label-chip-input"
-                                    #labelInput
-                                    [formControl]="labelCtrl"
-                                    [matChipInputFor]="chipGrid"
-                                    [matAutocomplete]="auto"
-                                    [matChipInputSeparatorKeyCodes]="
-                                        separatorKeysCodes
-                                    "
-                                    (matChipInputTokenEnd)="add($event)"
-                                    [disabled]="allLabels.length === 0"
-                                />
-                            </mat-chip-grid>
-                            <mat-autocomplete
-                                #auto="matAutocomplete"
-                                (optionSelected)="selected($event)"
-                            >
-                                @for (
-                                    label of filteredLabels | async;
-                                    track label
-                                ) {
-                                    <mat-option
-                                        [value]="label._id"
-                                        [attr.data-cy]="'option-' + 
label.label"
-                                        >{{ label.label }}</mat-option
-                                    >
-                                }
-                            </mat-autocomplete>
-                        </mat-form-field>
+                        </sp-search-select>
                     </sp-form-field>
                 }
             </div>
diff --git 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
index 5e4c9ab779..76f68c9ed8 100644
--- 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
+++ 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-details-panel/asset-details-basics/asset-details-labels/asset-details-labels.component.ts
@@ -18,12 +18,10 @@
 
 import {
     Component,
-    ElementRef,
     Input,
     OnChanges,
     OnInit,
     SimpleChanges,
-    ViewChild,
     inject,
 } from '@angular/core';
 import {
@@ -31,20 +29,10 @@ import {
     SpAsset,
     SpLabel,
 } from '@streampipes/platform-services';
-import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
-import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
-import { Observable } from 'rxjs';
-import {
-    MatAutocomplete,
-    MatAutocompleteSelectedEvent,
-    MatAutocompleteTrigger,
-} from '@angular/material/autocomplete';
-import { map, startWith } from 'rxjs/operators';
 import {
     FormFieldComponent,
-    SpColorizationService,
     SpLabelComponent,
+    SearchSelectComponent,
 } from '@streampipes/shared-ui';
 import {
     FlexDirective,
@@ -55,10 +43,6 @@ import {
 import { MatButton, MatIconButton } from '@angular/material/button';
 import { RouterLink } from '@angular/router';
 import { MatIcon } from '@angular/material/icon';
-import { MatFormField } from '@angular/material/form-field';
-import { AsyncPipe, NgStyle } from '@angular/common';
-import { StyleDirective } from '@ngbracket/ngx-layout/extended';
-import { MatOption } from '@angular/material/select';
 import { TranslatePipe } from '@ngx-translate/core';
 
 @Component({
@@ -74,23 +58,13 @@ import { TranslatePipe } from '@ngx-translate/core';
         RouterLink,
         MatIconButton,
         MatIcon,
-        MatFormField,
-        MatChipsModule,
-        NgStyle,
-        StyleDirective,
-        FormsModule,
-        MatAutocompleteTrigger,
-        ReactiveFormsModule,
-        MatAutocomplete,
-        MatOption,
+        SearchSelectComponent,
         SpLabelComponent,
-        AsyncPipe,
         TranslatePipe,
     ],
 })
 export class AssetDetailsLabelsComponent implements OnInit, OnChanges {
     private labelsService = inject(LabelsService);
-    private colorizationService = inject(SpColorizationService);
 
     @Input()
     asset: SpAsset;
@@ -99,16 +73,9 @@ export class AssetDetailsLabelsComponent implements OnInit, 
OnChanges {
     editMode: boolean;
 
     labels: SpLabel[] = [];
-    labelTextColors: Record<string, string> = {};
-
-    separatorKeysCodes: number[] = [ENTER, COMMA];
-    labelCtrl = new FormControl('');
-    filteredLabels: Observable<SpLabel[]>;
     allLabels: SpLabel[] = [];
     labelsAvailable = false;
 
-    @ViewChild('labelInput') labelInput: ElementRef<HTMLInputElement>;
-
     ngOnInit(): void {
         this.loadLabels();
     }
@@ -118,22 +85,9 @@ export class AssetDetailsLabelsComponent implements OnInit, 
OnChanges {
             this.allLabels = labels.sort((a, b) =>
                 a.label.localeCompare(b.label),
             );
-            labels.forEach(
-                label =>
-                    (this.labelTextColors[label._id] =
-                        this.colorizationService.generateContrastColor(
-                            label.color,
-                        )),
-            );
             this.refreshCurrentLabels();
             this.labelsAvailable = true;
-            this.updateFilteredLabels();
         });
-
-        this.filteredLabels = this.labelCtrl.valueChanges.pipe(
-            startWith(''),
-            map(value => this._filter(value as string)),
-        );
     }
 
     refreshCurrentLabels(): void {
@@ -150,67 +104,11 @@ export class AssetDetailsLabelsComponent implements 
OnInit, OnChanges {
     ngOnChanges(changes: SimpleChanges) {
         if (changes['asset'] && this.labelsAvailable) {
             this.refreshCurrentLabels();
-            this.updateFilteredLabels();
-        }
-    }
-
-    getAvailableLabels(): SpLabel[] {
-        return this.allLabels.filter(
-            label =>
-                !this.labels.some(
-                    selectedLabel => selectedLabel._id === label._id,
-                ),
-        );
-    }
-
-    add(event: MatChipInputEvent): void {
-        const value = (event.value || '').trim();
-        if (value) {
-            this.addLabelToSelection(value);
-        }
-        event.chipInput?.clear();
-        this.labelCtrl.setValue(null);
-    }
-
-    findLabel(value: string): SpLabel {
-        return this.allLabels.find(l => l._id === value);
-    }
-
-    remove(label: SpLabel): void {
-        const index = this.asset.labelIds.indexOf(label._id);
-        const labelsIndex = this.labels.findIndex(l => l._id === label._id);
-        if (index >= 0) {
-            this.labels.splice(labelsIndex, 1);
-            this.asset.labelIds.splice(index, 1);
         }
-        this.updateFilteredLabels();
-    }
-
-    selected(event: MatAutocompleteSelectedEvent): void {
-        this.addLabelToSelection(event.option.value);
-        this.labelInput.nativeElement.value = '';
-        this.labelCtrl.setValue(null);
-    }
-
-    addLabelToSelection(textLabel: string): void {
-        const label = this.findLabel(textLabel);
-        if (label && !this.labels.some(l => l._id === label._id)) {
-            this.labels.push(label);
-            this.asset.labelIds.push(label._id);
-        }
-    }
-
-    private _filter(value: string): SpLabel[] {
-        const filterValue = value.toLowerCase();
-        return this.getAvailableLabels().filter(label =>
-            label.label.toLowerCase().includes(filterValue),
-        );
     }
 
-    private updateFilteredLabels(): void {
-        this.filteredLabels = this.labelCtrl.valueChanges.pipe(
-            startWith(''),
-            map(value => this._filter(typeof value === 'string' ? value : '')),
-        );
+    onLabelsChange(labels: SpLabel | SpLabel[] | undefined): void {
+        this.labels = Array.isArray(labels) ? labels : [];
+        this.asset.labelIds = this.labels.map(label => label._id);
     }
 }
diff --git 
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
 
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
index 5206d4f22a..f0c5677634 100644
--- 
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
+++ 
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
@@ -274,7 +274,10 @@ export class ChartContainerComponent
                     if (
                         typeChange.widgetId === this.configuredWidget.elementId
                     ) {
-                        this.chooseWidget(typeChange.newWidgetTypeId);
+                        this.chooseWidget(
+                            typeChange.newWidgetTypeId,
+                            typeChange.deferInitialDataLoad,
+                        );
                     }
                 },
             );
@@ -322,11 +325,14 @@ export class ChartContainerComponent
         this.interval$?.unsubscribe();
     }
 
-    chooseWidget(widgetTypeId: string) {
+    chooseWidget(widgetTypeId: string, deferInitialDataLoad = false) {
         if (widgetTypeId != undefined && !this.showRequiresAttentionMessage) {
             const widgetToDisplay =
                 this.chartRegistryService.getChartTemplate(widgetTypeId);
-            this.loadComponent(widgetToDisplay.widgetComponent);
+            this.loadComponent(
+                widgetToDisplay.widgetComponent,
+                deferInitialDataLoad,
+            );
         }
     }
 
@@ -338,7 +344,7 @@ export class ChartContainerComponent
         );
     }
 
-    loadComponent(widgetToDisplay) {
+    loadComponent(widgetToDisplay, deferInitialDataLoad = false) {
         const container = this.el.nativeElement.querySelector(
             '.widget-content',
         ) as HTMLDivElement;
@@ -373,6 +379,7 @@ export class ChartContainerComponent
         this.componentRef.instance.dataExplorerWidget = this.configuredWidget;
         this.componentRef.instance.previewMode = this.previewMode;
         this.componentRef.instance.gridMode = this.gridMode;
+        this.componentRef.instance.deferInitialDataLoad = deferInitialDataLoad;
         this.componentRef.instance.widgetIndex = this.widgetIndex;
         this.componentRef.instance.observableGenerator =
             this.observableGenerator;
diff --git 
a/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
 
b/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
index e5bd69023e..889f7e200e 100644
--- 
a/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
+++ 
b/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts
@@ -86,6 +86,9 @@ export abstract class BaseDataExplorerWidgetDirective<
     @Input()
     previewMode = false;
 
+    @Input()
+    deferInitialDataLoad = false;
+
     @Input()
     gridMode = true;
 
@@ -213,7 +216,9 @@ export abstract class BaseDataExplorerWidgetDirective<
             },
         );
 
-        this.updateData();
+        if (!this.deferInitialDataLoad) {
+            this.updateData();
+        }
     }
 
     public cleanupSubscriptions(): void {
diff --git a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts 
b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
index b749ad6d95..68b3ce7dc3 100644
--- a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
+++ b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
@@ -49,6 +49,7 @@ export interface BaseWidgetData<T extends 
DataExplorerWidgetModel> {
     dataViewMode: boolean;
     previewMode: boolean;
     gridMode: boolean;
+    deferInitialDataLoad?: boolean;
     widgetIndex?: number;
     dashboardChartOverrides?: DashboardChartOverrides;
 
@@ -131,6 +132,7 @@ export interface WidgetBaseAppearanceConfig {
 export interface ChartTypeChangeMessage {
     widgetId: string;
     newWidgetTypeId: string;
+    deferInitialDataLoad?: boolean;
 }
 
 export interface RefreshMessage {
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
index bb64ea35d8..0f884ccaff 100644
--- 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
@@ -84,74 +84,20 @@
                             </div>
                         }
                         @if (availableMeasurements.length !== 0) {
-                            <mat-form-field
+                            <sp-search-select
                                 fxFlex="100"
-                                appearance="outline"
-                                subscriptSizing="dynamic"
+                                [items]="availableMeasurements"
+                                [value]="selectedMeasurement"
+                                [placeholder]="'Search datasets' | translate"
+                                dataCy="data-explorer-select-data-set"
+                                (valueChange)="
+                                    onMeasurementSelectionChange(
+                                        sourceConfig,
+                                        $event
+                                    )
+                                "
                             >
-                                <mat-icon matPrefix>search</mat-icon>
-                                <input
-                                    matInput
-                                    [placeholder]="
-                                        'Search datasets' | translate
-                                    "
-                                    [value]="measurementInputValue"
-                                    (input)="
-                                        onMeasurementSearchChange(
-                                            $any($event.target).value
-                                        )
-                                    "
-                                    [matAutocomplete]="datasetAutocomplete"
-                                    data-cy="data-explorer-select-data-set"
-                                />
-                                @if (hasActiveMeasurementSearch()) {
-                                    <button
-                                        mat-icon-button
-                                        matSuffix
-                                        type="button"
-                                        [attr.aria-label]="
-                                            'Clear search' | translate
-                                        "
-                                        (click)="clearMeasurementSearch()"
-                                    >
-                                        <mat-icon>close</mat-icon>
-                                    </button>
-                                }
-                                <mat-autocomplete
-                                    #datasetAutocomplete="matAutocomplete"
-                                    autoActiveFirstOption
-                                    (optionSelected)="
-                                        onMeasurementSelected(
-                                            sourceConfig,
-                                            $event
-                                        )
-                                    "
-                                >
-                                    @for (
-                                        measurement of filteredMeasurements;
-                                        track measurement.elementId
-                                    ) {
-                                        <mat-option
-                                            [value]="measurement.measureName"
-                                        >
-                                            <span class="font-medium 
text-sm">{{
-                                                measurement.measureName
-                                            }}</span>
-                                        </mat-option>
-                                    }
-                                    @if (
-                                        filteredMeasurements.length === 0 &&
-                                        hasActiveMeasurementSearch()
-                                    ) {
-                                        <mat-option disabled>
-                                            {{
-                                                'No datasets match your 
search.'
-                                                    | translate
-                                            }}
-                                        </mat-option>
-                                    }
-                                </mat-autocomplete>
-                            </mat-form-field>
+                            </sp-search-select>
                         }
                     </sp-split-section>
                     @if (sourceConfig.measureName) {
@@ -400,7 +346,7 @@
                                 #fieldSelectionPanel
                                 [sourceConfig]="sourceConfig"
                                 (initialFieldSelectionEvent)="
-                                    createDefaultWidget()
+                                    onInitialFieldSelection()
                                 "
                             ></sp-field-selection-panel>
                             <sp-filter-selection-panel
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
index a441cd8fb4..3d0e0e7690 100644
--- 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
@@ -20,6 +20,7 @@ import {
     Component,
     EventEmitter,
     Input,
+    OnDestroy,
     OnInit,
     Output,
     ViewChild,
@@ -58,32 +59,23 @@ import { MatTooltip } from '@angular/material/tooltip';
 import {
     FormFieldComponent,
     SpAlertBannerComponent,
+    SearchSelectComponent,
     SplitSectionComponent,
 } from '@streampipes/shared-ui';
-import {
-    MatFormField,
-    MatPrefix,
-    MatSuffix,
-} from '@angular/material/form-field';
-import { MatOption } from '@angular/material/core';
+import { MatFormField } from '@angular/material/form-field';
 import { MatIcon } from '@angular/material/icon';
 import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
 import { FormsModule } from '@angular/forms';
 import { NgClass } from '@angular/common';
 import { ClassDirective } from '@ngbracket/ngx-layout/extended';
-import { MatInput } from '@angular/material/input';
 import { MatCheckbox } from '@angular/material/checkbox';
+import { MatInput } from '@angular/material/input';
 import { AggregateConfigurationComponent } from 
'./aggregate-configuration/aggregate-configuration.component';
 import { FillConfigurationComponent } from 
'./fill-configuration/fill-configuration.component';
 import { FilterSelectionPanelComponent } from 
'./filter-selection-panel/filter-selection-panel.component';
 import { OrderSelectionPanelComponent } from 
'./order-selection-panel/order-selection-panel.component';
 import { ResultLabelConfigurationComponent } from 
'./result-label-configuration/result-label-configuration.component';
 import { TranslatePipe } from '@ngx-translate/core';
-import {
-    MatAutocomplete,
-    MatAutocompleteSelectedEvent,
-    MatAutocompleteTrigger,
-} from '@angular/material/autocomplete';
 
 @Component({
     selector: 'sp-chart-data-settings',
@@ -103,9 +95,6 @@ import {
         LayoutGapDirective,
         MatButton,
         MatFormField,
-        MatPrefix,
-        MatSuffix,
-        MatOption,
         MatIcon,
         MatRadioGroup,
         FormsModule,
@@ -113,10 +102,9 @@ import {
         ClassDirective,
         MatRadioButton,
         FormFieldComponent,
-        MatInput,
         MatCheckbox,
-        MatAutocomplete,
-        MatAutocompleteTrigger,
+        MatInput,
+        SearchSelectComponent,
         AggregateConfigurationComponent,
         FillConfigurationComponent,
         FieldSelectionPanelComponent,
@@ -127,7 +115,7 @@ import {
         TranslatePipe,
     ],
 })
-export class ChartDataSettingsComponent implements OnInit {
+export class ChartDataSettingsComponent implements OnInit, OnDestroy {
     private datalakeRestService = inject(DatalakeRestService);
     private widgetConfigService = inject(ChartConfigurationService);
     private fieldProviderService = inject(ChartFieldProviderService);
@@ -156,8 +144,10 @@ export class ChartDataSettingsComponent implements OnInit {
     groupSelectionPanel: GroupSelectionPanelComponent;
 
     availableMeasurements: DatasetSummaryDto[] = [];
-    filteredMeasurements: DatasetSummaryDto[] = [];
-    measurementInputValue = '';
+    selectedMeasurement: DatasetSummaryDto | undefined;
+
+    private pendingMeasurementRefresh = false;
+    private dataRefreshTimeout?: ReturnType<typeof setTimeout>;
 
     step = 0;
 
@@ -173,17 +163,24 @@ export class ChartDataSettingsComponent implements OnInit 
{
         this.loadPipelinesAndMeasurements();
     }
 
+    ngOnDestroy(): void {
+        clearTimeout(this.dataRefreshTimeout);
+    }
+
     loadPipelinesAndMeasurements() {
         this.datalakeRestService.getMeasurementSummary().subscribe(response => 
{
             this.availableMeasurements = response.resources.sort((a, b) =>
                 a.measureName.localeCompare(b.measureName),
             );
-            this.applyMeasurementSearch();
+            this.syncSelectedMeasurementSummary();
 
             if (!this.sourceConfig) {
                 const defaultConfigs = this.findDefaultConfig();
                 this.initializeSourceConfig(defaultConfigs.measureName);
                 if (defaultConfigs.measureName !== undefined) {
+                    this.selectedMeasurement = this.findMeasurementSummary(
+                        defaultConfigs.measureName,
+                    );
                     this.loadMeasurement(
                         defaultConfigs.measureName,
                         true,
@@ -229,45 +226,35 @@ export class ChartDataSettingsComponent implements OnInit 
{
 
     updateMeasure(sourceConfig: SourceConfig, measureName: string) {
         sourceConfig.measureName = measureName;
-        this.measurementInputValue = measureName;
+        this.selectedMeasurement = this.findMeasurementSummary(measureName);
         this.loadMeasurement(measureName, true, true);
     }
 
-    onMeasurementSearchChange(value: string): void {
-        this.measurementInputValue = value;
-        this.applyMeasurementSearch();
-    }
-
-    clearMeasurementSearch(): void {
-        this.measurementInputValue = '';
-        this.applyMeasurementSearch();
-    }
-
-    hasActiveMeasurementSearch(): boolean {
-        return this.measurementInputValue.trim().length > 0;
-    }
-
-    onMeasurementSelected(
+    onMeasurementSelectionChange(
         sourceConfig: SourceConfig,
-        event: MatAutocompleteSelectedEvent,
+        selectedMeasurement:
+            | DatasetSummaryDto
+            | DatasetSummaryDto[]
+            | undefined,
     ): void {
-        this.updateMeasure(sourceConfig, event.option.value);
-    }
+        if (Array.isArray(selectedMeasurement)) {
+            return;
+        }
 
-    private applyMeasurementSearch(): void {
-        const query = this.measurementInputValue.trim().toLowerCase();
-        if (!query) {
-            this.filteredMeasurements = this.availableMeasurements;
+        if (!selectedMeasurement) {
+            this.clearMeasure(sourceConfig);
             return;
         }
 
-        this.filteredMeasurements = this.availableMeasurements.filter(
-            measurement =>
-                measurement.measureName.toLowerCase().includes(query) ||
-                measurement.pipelines.some(pipeline =>
-                    pipeline.toLowerCase().includes(query),
-                ),
-        );
+        this.updateMeasure(sourceConfig, selectedMeasurement.measureName);
+    }
+
+    private clearMeasure(sourceConfig: SourceConfig): void {
+        sourceConfig.measureName = '';
+        sourceConfig.measure = undefined;
+        sourceConfig.queryConfig.fields = [];
+        sourceConfig.queryConfig.groupBy = [];
+        this.selectedMeasurement = undefined;
     }
 
     private loadMeasurement(
@@ -300,12 +287,15 @@ export class ChartDataSettingsComponent implements OnInit 
{
         this.dataLakeMeasureChange.emit(measure);
         sourceConfig.measureName = measure.measureName;
         sourceConfig.measure = measure;
-        this.measurementInputValue = measure.measureName;
+        this.selectedMeasurement = this.findMeasurementSummary(
+            measure.measureName,
+        );
 
         if (!resetQueryConfig) {
             return;
         }
 
+        this.pendingMeasurementRefresh = refreshData;
         sourceConfig.queryConfig.fields = [];
         if (this.fieldSelectionPanel) {
             this.fieldSelectionPanel.applyDefaultFields();
@@ -316,8 +306,8 @@ export class ChartDataSettingsComponent implements OnInit {
             this.groupSelectionPanel.applyDefaultFields();
         }
 
-        if (refreshData) {
-            this.triggerDataRefresh();
+        if (refreshData && this.fieldSelectionPanel) {
+            this.scheduleDataRefresh();
         }
     }
 
@@ -325,12 +315,32 @@ export class ChartDataSettingsComponent implements OnInit 
{
         if (this.sourceConfig?.measure) {
             this.dataLakeMeasure = this.sourceConfig.measure;
             this.dataLakeMeasureChange.emit(this.sourceConfig.measure);
-            this.measurementInputValue = this.sourceConfig.measure.measureName;
+            this.selectedMeasurement = this.findMeasurementSummary(
+                this.sourceConfig.measure.measureName,
+            );
         } else if (this.sourceConfig?.measureName) {
-            this.measurementInputValue = this.sourceConfig.measureName;
+            this.selectedMeasurement = this.findMeasurementSummary(
+                this.sourceConfig.measureName,
+            );
+        }
+    }
+
+    private syncSelectedMeasurementSummary(): void {
+        if (this.sourceConfig?.measureName) {
+            this.selectedMeasurement = this.findMeasurementSummary(
+                this.sourceConfig.measureName,
+            );
         }
     }
 
+    private findMeasurementSummary(
+        measureName: string,
+    ): DatasetSummaryDto | undefined {
+        return this.availableMeasurements.find(
+            measurement => measurement.measureName === measureName,
+        );
+    }
+
     changeDataAggregation() {
         this.fieldSelectionPanel.applyDefaultFields();
         this.triggerDataRefresh();
@@ -368,7 +378,14 @@ export class ChartDataSettingsComponent implements OnInit {
         };
     }
 
-    createDefaultWidget(): void {
+    onInitialFieldSelection(): void {
+        const defaultWidgetCreated = this.createDefaultWidget();
+        if (defaultWidgetCreated || this.pendingMeasurementRefresh) {
+            this.scheduleDataRefresh();
+        }
+    }
+
+    createDefaultWidget(): boolean {
         if (this.checkIfDefaultTableShouldBeShown()) {
             const fields = this.fieldProviderService.generateFieldLists(
                 this.dataConfig.sourceConfigs,
@@ -379,13 +396,18 @@ export class ChartDataSettingsComponent implements OnInit 
{
             this.widgetTypeService.notify({
                 widgetId: this.currentlyConfiguredWidget.elementId,
                 newWidgetTypeId: this.currentlyConfiguredWidget.widgetType,
+                deferInitialDataLoad: true,
             });
 
             this.createWidgetEmitter.emit({
                 a: this.dataLakeMeasure,
                 b: this.currentlyConfiguredWidget,
             });
+
+            return true;
         }
+
+        return false;
     }
 
     /**
@@ -404,6 +426,12 @@ export class ChartDataSettingsComponent implements OnInit {
         });
     }
 
+    private scheduleDataRefresh(): void {
+        this.pendingMeasurementRefresh = false;
+        clearTimeout(this.dataRefreshTimeout);
+        this.dataRefreshTimeout = setTimeout(() => this.triggerDataRefresh());
+    }
+
     toggleExpandFieldsDataSource() {
         this.expandFieldsDataSource = !this.expandFieldsDataSource;
     }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.html
 
b/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.html
index 85b7c1b9fe..b25614c70a 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.html
@@ -37,43 +37,14 @@
     @if (deploymentMode === 'filter') {
         <div fxFlex="100" fxLayout="column" class="mt-10">
             <sp-form-field [level]="3" label="Service Tags">
-                <mat-form-field color="accent">
-                    <mat-chip-grid #chipGrid>
-                        @for (
-                            serviceTag of 
deploymentConfiguration.desiredServiceTags;
-                            track serviceTag
-                        ) {
-                            <mat-chip-row (removed)="remove(serviceTag.value)">
-                                {{ serviceTag.value }}
-                                <button matChipRemove>
-                                    <mat-icon>cancel</mat-icon>
-                                </button>
-                            </mat-chip-row>
-                        }
-                        <input
-                            placeholder="Add service tag"
-                            #serviceTagInput
-                            [formControl]="serviceTagCtrl"
-                            [matChipInputFor]="chipGrid"
-                            [matAutocomplete]="auto"
-                            
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
-                            (matChipInputTokenEnd)="add($event)"
-                        />
-                    </mat-chip-grid>
-                    <mat-autocomplete
-                        #auto="matAutocomplete"
-                        (optionSelected)="selected($event)"
-                    >
-                        @for (
-                            serviceTag of filteredServiceTags | async;
-                            track serviceTag
-                        ) {
-                            <mat-option [value]="serviceTag">
-                                {{ serviceTag }}
-                            </mat-option>
-                        }
-                    </mat-autocomplete>
-                </mat-form-field>
+                <sp-search-select
+                    [items]="availableServiceTags"
+                    [value]="deploymentConfiguration.desiredServiceTags"
+                    [multiple]="true"
+                    [placeholder]="'Add service tag' | translate"
+                    (valueChange)="onServiceTagsChange($event)"
+                >
+                </sp-search-select>
             </sp-form-field>
         </div>
     }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.ts
index 852e20378c..147fe632bd 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-settings/adapter-deployment-settings/adapter-deployment-settings.component.ts
@@ -16,41 +16,24 @@
  *
  */
 
-import {
-    Component,
-    ElementRef,
-    inject,
-    Input,
-    OnInit,
-    ViewChild,
-} from '@angular/core';
+import { Component, inject, Input, OnInit } from '@angular/core';
 import {
     ExtensionDeploymentConfiguration,
     ServiceTagService,
     SpServiceTag,
 } from '@streampipes/platform-services';
-import { Observable } from 'rxjs';
-import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
-import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
-import { map, startWith } from 'rxjs/operators';
-import {
-    MatAutocomplete,
-    MatAutocompleteSelectedEvent,
-    MatAutocompleteTrigger,
-} from '@angular/material/autocomplete';
+import { FormsModule } from '@angular/forms';
 import {
     MatRadioButton,
     MatRadioChange,
     MatRadioGroup,
 } from '@angular/material/radio';
 import { FlexDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex';
-import { MatFormField } from '@angular/material/form-field';
-import { MatIcon } from '@angular/material/icon';
-import { MatOption } from '@angular/material/select';
-import { AsyncPipe } from '@angular/common';
 import { TranslatePipe } from '@ngx-translate/core';
-import { FormFieldComponent } from '@streampipes/shared-ui';
+import {
+    FormFieldComponent,
+    SearchSelectComponent,
+} from '@streampipes/shared-ui';
 
 @Component({
     selector: 'sp-adapter-deployment-settings',
@@ -61,16 +44,9 @@ import { FormFieldComponent } from '@streampipes/shared-ui';
         MatRadioGroup,
         FormsModule,
         MatRadioButton,
-        MatFormField,
-        MatChipsModule,
-        MatIcon,
-        MatAutocompleteTrigger,
-        ReactiveFormsModule,
-        MatAutocomplete,
-        MatOption,
-        AsyncPipe,
         TranslatePipe,
         FormFieldComponent,
+        SearchSelectComponent,
     ],
 })
 export class SpAdapterDeploymentSettingsComponent implements OnInit {
@@ -80,81 +56,26 @@ export class SpAdapterDeploymentSettingsComponent 
implements OnInit {
     deploymentConfiguration: ExtensionDeploymentConfiguration;
 
     availableServiceTags: SpServiceTag[] = [];
-    availableServiceTagValues: string[] = [];
 
     deploymentMode = 'all';
 
-    separatorKeysCodes: number[] = [ENTER, COMMA];
-    serviceTagCtrl = new FormControl('');
-    filteredServiceTags: Observable<string[]>;
-
-    @ViewChild('serviceTagInput') serviceTagInput: 
ElementRef<HTMLInputElement>;
-
-    constructor() {
-        this.filteredServiceTags = this.serviceTagCtrl.valueChanges.pipe(
-            startWith(null),
-            map((serviceTagValue: string | null) => {
-                return serviceTagValue
-                    ? this._filter(serviceTagValue)
-                    : this.availableServiceTagValues.slice();
-            }),
-        );
-    }
-
     ngOnInit(): void {
         if (this.deploymentConfiguration.desiredServiceTags.length > 0) {
             this.deploymentMode = 'filter';
         }
         this.serviceTagService.getCustomServiceTags().subscribe(res => {
             this.availableServiceTags = res;
-            this.availableServiceTagValues = res.map(st => st.value);
         });
     }
 
-    add(event: MatChipInputEvent): void {
-        const value = (event.value || '').trim();
-
-        if (value) {
-            this.deploymentConfiguration.desiredServiceTags.push(
-                this.findTag(value),
-            );
-        }
-
-        if (event.chipInput) {
-            event.chipInput.clear();
-        }
-
-        this.serviceTagCtrl.setValue(null);
-    }
-
-    findTag(value: string): SpServiceTag {
-        return this.availableServiceTags.find(st => st.value === value);
-    }
-
-    remove(serviceTag: string): void {
-        const index = 
this.deploymentConfiguration.desiredServiceTags.findIndex(
-            st => st.value === serviceTag,
-        );
-
-        if (index >= 0) {
-            this.deploymentConfiguration.desiredServiceTags.splice(index, 1);
-        }
-    }
-
-    selected(event: MatAutocompleteSelectedEvent): void {
-        this.deploymentConfiguration.desiredServiceTags.push(
-            this.findTag(event.option.viewValue),
-        );
-        this.serviceTagInput.nativeElement.value = '';
-        this.serviceTagCtrl.setValue(null);
-    }
-
-    private _filter(value: string): string[] {
-        const filterValue = value.toLowerCase();
-
-        return this.availableServiceTagValues.filter(st =>
-            st.toLowerCase().includes(filterValue),
-        );
+    onServiceTagsChange(
+        serviceTags: SpServiceTag | SpServiceTag[] | undefined,
+    ): void {
+        this.deploymentConfiguration.desiredServiceTags = Array.isArray(
+            serviceTags,
+        )
+            ? serviceTags
+            : [];
     }
 
     handleSelectionChange(event: MatRadioChange): void {

Reply via email to