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 {