From b63c73b0f8a5213cd018fbaf62bc340b3f781524 Mon Sep 17 00:00:00 2001 From: Oleksandra Kalinina <946369+korel-san@users.noreply.github.com> Date: Tue, 20 Dec 2022 11:02:57 +0200 Subject: [PATCH] feature #177 Create component search with custom filter tokens --- ui/angular.json | 2 - .../execution-events-query.models.ts | 45 ++- .../src/lib/execution-event/services/index.ts | 6 +- .../services/label-api.service.ts | 49 +++ .../execution-event/services/public-api.ts | 1 + .../spline-api-execution-event.module.ts | 8 +- ...line-search-box-with-filter.component.html | 44 ++- ...spline-search-box-with-filter.component.ts | 288 +++++++++++------- .../spline-search-box.component.html | 50 +-- .../search-box/spline-search-box.component.ts | 48 +-- .../search-box/spline-search-box.module.ts | 7 +- .../components/_search-box.component.scss | 4 +- .../spline-attribute-search.component.ts | 1 - .../spline-search-dynamic-table-store.ns.ts | 1 - ...spline-search-dynamic-table.component.html | 3 +- .../spline-search-dynamic-table.component.ts | 24 +- .../src/spline-dynamic-table-shared.module.ts | 1 + .../src/data-sources/events.factory-store.ts | 5 +- .../dynamic-filter.store-extras.ts | 1 - .../models/query/query-params.models.ts | 14 +- .../search-query/search.factory-store.ts | 32 +- ui/src/app/app.module.ts | 6 +- 22 files changed, 405 insertions(+), 235 deletions(-) create mode 100644 ui/projects/spline-api/src/lib/execution-event/services/label-api.service.ts diff --git a/ui/angular.json b/ui/angular.json index 619ee990..811c8dcc 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -197,8 +197,6 @@ "browserTarget": "spline-ui:build:production" }, "localhost": { - "sourceMap": true, - "optimization": false, "browserTarget": "spline-ui:build:localhost" } } diff --git a/ui/projects/spline-api/src/lib/execution-event/models/entities/execution-event/execution-events-query.models.ts b/ui/projects/spline-api/src/lib/execution-event/models/entities/execution-event/execution-events-query.models.ts index 93fc1cfd..13425332 100644 --- a/ui/projects/spline-api/src/lib/execution-event/models/entities/execution-event/execution-events-query.models.ts +++ b/ui/projects/spline-api/src/lib/execution-event/models/entities/execution-event/execution-events-query.models.ts @@ -15,7 +15,15 @@ */ import { HttpParams } from '@angular/common/http' -import { DEFAULT_PAGE_LIMIT, PageQueryParams, QueryPager, QuerySorter, SearchQuery } from 'spline-utils' +import { + DEFAULT_PAGE_LIMIT, + LabelPageQueryParams, + LabelValuesPageQueryParams, + PageQueryParams, + QueryPager, + QuerySorter, + SearchQuery +} from 'spline-utils' import { DataSourceWriteMode } from '../data-source' @@ -28,6 +36,7 @@ export namespace ExecutionEventsQuery { executedAtFrom?: Date executedAtTo?: Date searchTerm?: string + label?: string[], dataSourceUri?: string asAtTime?: number applicationId?: string @@ -42,6 +51,7 @@ export namespace ExecutionEventsQuery { sortOrder?: string sortField?: string searchTerm?: string + label?: string[] pageSize?: number pageNum?: number dataSourceUri?: string @@ -54,11 +64,11 @@ export namespace ExecutionEventsQuery { return { ...(queryParams?.filter ? toQueryFilterDto(queryParams.filter) : {}), ...(queryParams?.pager ? toQueryPagerDto(queryParams.pager) : {}), - ...(queryParams?.sortBy?.length ? toSortingDto(queryParams.sortBy) : {}), + ...(queryParams?.sortBy?.length ? toSortingDto(queryParams.sortBy) : {}) } } - export function queryParamsDtoToHttpParams(queryParamsDto: QueryParamsDto): HttpParams { + export function queryParamsDtoToHttpParams(queryParamsDto: QueryParamsDto | LabelPageQueryParams | LabelValuesPageQueryParams): HttpParams { let httpParams = new HttpParams() Object.keys(queryParamsDto) .filter(key => queryParamsDto[key] !== undefined) @@ -68,37 +78,52 @@ export namespace ExecutionEventsQuery { return httpParams } + export function labelQueryParamsToHttpParams(queryParams: LabelPageQueryParams | LabelValuesPageQueryParams): HttpParams { + return queryParamsDtoToHttpParams(queryParams) + } + export function queryParamsToHttpParams(queryParams: QueryParams): HttpParams { const queryParamsDto = toQueryParamsDto(queryParams) return queryParamsDtoToHttpParams(queryParamsDto) } export function toQueryParams( - searchParams: SearchQuery.SearchParams, + searchParams: SearchQuery.SearchParams ): QueryParams { const queryFilter = { ...searchParams.filter, ...searchParams.alwaysOnFilter, - searchTerm: searchParams.searchTerm, + searchTerm: searchParams.searchTerm } return { filter: queryFilter, pager: searchParams.pager, - sortBy: searchParams.sortBy, + sortBy: searchParams.sortBy } } + export function toLabelQueryParams( + searchString: string, labelName?: string + ): LabelPageQueryParams | LabelValuesPageQueryParams { + return { + length: -1, + offset: 0, + search: searchString, + labelName + } + } function toQueryFilterDto(queryFilter: QueryFilter): Partial { return { timestampStart: queryFilter?.executedAtFrom ? queryFilter?.executedAtFrom.getTime() : undefined, timestampEnd: queryFilter?.executedAtTo ? queryFilter?.executedAtTo.getTime() : undefined, searchTerm: queryFilter?.searchTerm?.length ? queryFilter?.searchTerm : undefined, + label: queryFilter?.label.length ? queryFilter?.label : undefined, dataSourceUri: queryFilter?.dataSourceUri, asAtTime: queryFilter?.asAtTime, applicationId: queryFilter?.applicationId, append: queryFilter?.writeMode?.length - ? queryFilter?.writeMode.map(m => { + ? queryFilter?.writeMode.map(m => { switch (m) { case DataSourceWriteMode.Append: return true @@ -108,7 +133,7 @@ export namespace ExecutionEventsQuery { return null // (append === null) means no writes, i.e. read-only items. } }) - : undefined + : undefined } } @@ -117,14 +142,14 @@ export namespace ExecutionEventsQuery { const offset = queryPager?.offset ?? 0 return { pageSize: limit, - pageNum: offset / limit + 1, + pageNum: offset / limit + 1 } } function toSortingDto(sortBy: QuerySorter.FieldSorter[]): Partial { return { sortField: sortBy[0].field, - sortOrder: sortBy[0].dir.toLowerCase(), + sortOrder: sortBy[0].dir.toLowerCase() } } } diff --git a/ui/projects/spline-api/src/lib/execution-event/services/index.ts b/ui/projects/spline-api/src/lib/execution-event/services/index.ts index d738ce95..cab5c73c 100644 --- a/ui/projects/spline-api/src/lib/execution-event/services/index.ts +++ b/ui/projects/spline-api/src/lib/execution-event/services/index.ts @@ -17,14 +17,16 @@ import { AttributeApiService } from './attribute-api.service' import { ExecutionEventApiService } from './execution-event-api.service' import { ExecutionPlanApiService } from './execution-plan-api.service' +import { LabelApiService } from './label-api.service' import { SplineDataSourceApiService } from './spline-data-source-api.service' -export const executionEventServices: any[] = [ +export const commonServiceProvider: any[] = [ ExecutionEventApiService, ExecutionPlanApiService, AttributeApiService, - SplineDataSourceApiService + SplineDataSourceApiService, + LabelApiService ] export * from './public-api' diff --git a/ui/projects/spline-api/src/lib/execution-event/services/label-api.service.ts b/ui/projects/spline-api/src/lib/execution-event/services/label-api.service.ts new file mode 100644 index 00000000..9cc9dd98 --- /dev/null +++ b/ui/projects/spline-api/src/lib/execution-event/services/label-api.service.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 ABSA Group Limited + * + * Licensed 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 { HttpClient, HttpErrorResponse } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { Observable, throwError } from 'rxjs' +import { catchError } from 'rxjs/operators' +import { LabelPageQueryParams, LabelValuesPageQueryParams } from 'spline-utils' + +import { ExecutionEventsQuery } from '../models' + +import { BaseApiService } from './base-api.service' + + +@Injectable() +export class LabelApiService extends BaseApiService { + + constructor(protected readonly http: HttpClient) { + super(http) + } + + fetchList(queryParams: LabelPageQueryParams | LabelValuesPageQueryParams): Observable { + // Apadter LabelQuery to LabelHttpParams + const params = ExecutionEventsQuery.labelQueryParamsToHttpParams(queryParams) + const url = this.getConsumerApiResourceURL(`labels/names${ ('labelName' in queryParams) && queryParams.labelName + ? `/${ queryParams.labelName }/values` + : '' }`) + return this.http.get(url, { params: params }) + .pipe( + catchError((error: HttpErrorResponse) => { + console.error(error) + return throwError(error) + }) + ) + } +} diff --git a/ui/projects/spline-api/src/lib/execution-event/services/public-api.ts b/ui/projects/spline-api/src/lib/execution-event/services/public-api.ts index 2c3a3d2e..3dcf5bcf 100644 --- a/ui/projects/spline-api/src/lib/execution-event/services/public-api.ts +++ b/ui/projects/spline-api/src/lib/execution-event/services/public-api.ts @@ -19,3 +19,4 @@ export * from './base-api.service' export * from './execution-event-api.service' export * from './spline-data-source-api.service' export * from './execution-plan-api.service' +export * from './label-api.service' diff --git a/ui/projects/spline-api/src/lib/execution-event/spline-api-execution-event.module.ts b/ui/projects/spline-api/src/lib/execution-event/spline-api-execution-event.module.ts index 824487eb..d2e5400b 100644 --- a/ui/projects/spline-api/src/lib/execution-event/spline-api-execution-event.module.ts +++ b/ui/projects/spline-api/src/lib/execution-event/spline-api-execution-event.module.ts @@ -20,15 +20,15 @@ import { SplineApiCoreModule } from '../core' import * as fromServices from './services' - +// TODO: Rename this module, due to not correspondent content to name convention @NgModule({ providers: [ - ...fromServices.executionEventServices, + ...fromServices.commonServiceProvider ], imports: [ - SplineApiCoreModule, + SplineApiCoreModule ], - exports: [], + exports: [] }) export class SplineApiExecutionEventModule { } diff --git a/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.html b/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.html index 107cdfc1..dffc729f 100644 --- a/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.html +++ b/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.html @@ -14,20 +14,44 @@ ~ limitations under the License. --> - - + - + - +
@@ -40,14 +64,14 @@ -

{{'COMMON.NO_OPTIONS_FOUND' | translate }}

- +

{{'COMMON.LOADING' | translate }} diff --git a/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.ts b/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.ts index bcf91684..f60032b0 100644 --- a/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.ts +++ b/ui/projects/spline-common/main/src/common/component/search-box-with-filter/spline-search-box-with-filter.component.ts @@ -14,33 +14,50 @@ * limitations under the License. */ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { FormControl, FormGroup } from '@angular/forms' import { MatAutocompleteTrigger } from '@angular/material/autocomplete' import _ from 'lodash' -import { BehaviorSubject, combineLatest, Observable, ObservedValueOf } from 'rxjs' -import { AttributeSearchRecord } from 'spline-api' +import { BehaviorSubject } from 'rxjs' +import { debounceTime, takeUntil, tap } from 'rxjs/operators' +import { AttributeSearchRecord, ExecutionEventsQuery, LabelApiService } from 'spline-api' import { BaseComponent, SearchFactoryStore } from 'spline-utils' - +// `job user:test example` - search input +// `job example` - search token +// `user:test` - filter token +// `user` - filter token key, filter token fragment (left) +// `test` - filter token value, filter token fragment (right) +// interface AutocompleteSelectingOption { optionType?: string optionValue: string } -interface AutocompleteSearchFilter { - partialSearchFilter: string - searchFilterType: string - startSearchFilterIndex: number - searchFilterDelimiter: string +interface SearchFilterTokenFragment { + filterTokenFragmentString: string + filterTokenFragmentType: string + filterTokenFragmentDelimiter: string + filterTokenKey?: string + startIndexOfFilterTokenFragment: number +} + +interface ParsedSearchInput { + searchToken: string + searchFilterTokenFragment: SearchFilterTokenFragment + filterTokens?: Map> } -interface SearchTextInput { - searchString: string - autocompleteFilterInput: AutocompleteSearchFilter +enum AUTOCOMPLETE_STATE { + LOADED = 'loaded', + LOADING = 'loading', + ERROR = 'error' } -interface SearchFiltersInput { - searchFilters?: ReadonlyMap +enum RESERVED_FILTERS_DELIMITERS { + COLON_STRING = ':', + SPACE_STRING = ' ', + EMPTY_STRING = '' } @Component({ @@ -48,62 +65,70 @@ interface SearchFiltersInput { templateUrl: './spline-search-box-with-filter.component.html' }) export class SplineSearchBoxWithFilterComponent extends BaseComponent { - @Input() filterConfig = { - filtersTypeByDelimiters: { - ':': 'LabelValue', - ' ': 'LabelName' - }, - defaultDelimiter: ' ' - } + @ViewChild('inputRef') inputRef: ElementRef @ViewChild(MatAutocompleteTrigger) matAutocompleteTrigger: MatAutocompleteTrigger + + autocompleteProcessing$ = new BehaviorSubject(AUTOCOMPLETE_STATE.LOADED) + readonly autocompleteFilters$: BehaviorSubject = new BehaviorSubject([]) + readonly lastFoundFilterTokenKey$ = new BehaviorSubject('') @Input() placeholder = 'COMMON.SEARCH' - @Input() searchTerm = '' @Input() dataSource: SearchFactoryStore - @Output() search = new EventEmitter() + @Input() labelApiService: LabelApiService + @Input() searchDefaultString = '' + @Output() clearSearch = new EventEmitter() + @Output() search = new EventEmitter<{ searchToken: string, filterTokens: Map> }>() @Output() autocompleteSelecting = new EventEmitter() - @Output() autocompleteFetching - - readonly emitSearchEventDebounceTimeInUs = 100 isSearchFocused = false - readonly lastSearchInput$ = new BehaviorSubject('') - private readonly _searchString$ = new BehaviorSubject('') - private readonly autocompleteSearchFilter$ = new BehaviorSubject({ - partialSearchFilter: '', - searchFilterType: this.filterConfig.filtersTypeByDelimiters[this.filterConfig.defaultDelimiter], - startSearchFilterIndex: 0, - searchFilterDelimiter: this.filterConfig.defaultDelimiter + emitSearchEventDebounceTimeInUs = 300 + autocompleteStateEnum = AUTOCOMPLETE_STATE + formGroup = new FormGroup({ + searchControl: new FormControl() + }) + autocompleteSearchFilter$ = new BehaviorSubject({ + filterTokenFragmentString: '', + filterTokenFragmentType: 'key', + startIndexOfFilterTokenFragment: 0, + filterTokenFragmentDelimiter: RESERVED_FILTERS_DELIMITERS.EMPTY_STRING }) - private readonly _searchFilters$ = new BehaviorSubject>(this.searchFiltersMap) + private lastSearchInput$ = new BehaviorSubject('') constructor() { super() - this.lastSearchInput$.next(this.searchTerm) - // this.search.emit(this.parseSearchInput(this.searchTerm)) - } - - private _searchFiltersMap: ReadonlyMap = new Map() - - get searchFiltersMap(): ReadonlyMap { - return this._searchFiltersMap - } - - @Output() get search$(): Observable<[ObservedValueOf>, ObservedValueOf>>]> { - return combineLatest([this._searchString$.asObservable(), this._searchFilters$.asObservable()]) + this.lastSearchInput$.next(this.searchDefaultString) + this.formGroup.get('searchControl').valueChanges + .pipe( + // wait some time between keyUp events + debounceTime(this.emitSearchEventDebounceTimeInUs), + // emit only different value form the previous one + // distinctUntilChanged((a, b) => a.trim().toLocaleLowerCase() === b.trim().toLocaleLowerCase()), + takeUntil(this.destroyed$) + ) + .subscribe(value => { + // @ts-ignore + this.inputRef.nativeElement.value = value + this.changeSearch(value) + } + ) + this.autocompleteSearchFilter$.subscribe((searchFilterTokenFragment) => { + this.setSearchFilterTokenFragment(searchFilterTokenFragment) + }) + this.autocompleteProcessing$.subscribe((value) => { + if (value === AUTOCOMPLETE_STATE.LOADING) { + this.matAutocompleteTrigger.openPanel() + } + }) } changeSearch(searchInput: string) { - debugger; this.lastSearchInput$.next(searchInput) - const { searchString, autocompleteFilterInput, searchFilters } = this.parseSearchInput(searchInput) - this.autocompleteSearchFilter$.next(autocompleteFilterInput) - if (!!autocompleteFilterInput.partialSearchFilter) { - this.matAutocompleteTrigger.openPanel() - } - else { - this._searchFiltersMap = searchFilters - this._searchString$.next(searchString) - this._searchFilters$.next(this.searchFiltersMap) + const { searchToken, searchFilterTokenFragment, filterTokens } = this.parseSearchInput(searchInput) + this.autocompleteSearchFilter$.next(searchFilterTokenFragment) + const isFilterTokenFragmentDetected = !!searchFilterTokenFragment.filterTokenFragmentString || + !!searchFilterTokenFragment.filterTokenKey + if (isFilterTokenFragmentDetected) { + this.autocompleteProcessing$.next(AUTOCOMPLETE_STATE.LOADING) } + this.search.emit({ searchToken, filterTokens }) this.isSearchFocused = true } @@ -111,86 +136,123 @@ export class SplineSearchBoxWithFilterComponent extends Ba onAutocompleteOptionSelected({ option: { value: optionValue } }): void { const searchInput = this.lastSearchInput$.getValue() - const { startSearchFilterIndex, searchFilterDelimiter }: AutocompleteSearchFilter = this.autocompleteSearchFilter$.getValue() - const permanentSearchInputPart = startSearchFilterIndex ? searchInput.slice(0, startSearchFilterIndex) + searchFilterDelimiter : '' - const newAutocompletedFilter = optionValue.match(/\s/g) ? `"${ optionValue }"` : optionValue - const searchInputWithSelectedOption = permanentSearchInputPart + newAutocompletedFilter + const { + startIndexOfFilterTokenFragment, + filterTokenFragmentDelimiter, + filterTokenFragmentType + }: SearchFilterTokenFragment = this.autocompleteSearchFilter$.getValue() + const permanentSearchInputPart = startIndexOfFilterTokenFragment ? searchInput.slice(0, startIndexOfFilterTokenFragment) + + filterTokenFragmentDelimiter : '' + const autocompletedFilterTokenFragment = optionValue.match(/\s/g) ? `"${ optionValue }"` : optionValue + const autocompletedFilterToken = filterTokenFragmentType === 'key' + ? `${ autocompletedFilterTokenFragment }:` + : `${ autocompletedFilterTokenFragment } ` + const searchInputWithSelectedOption = permanentSearchInputPart + autocompletedFilterToken this.lastSearchInput$.next(searchInputWithSelectedOption) - this.searchTerm = searchInputWithSelectedOption this.isSearchFocused = false - debugger; + this.formGroup.get('searchControl').setValue(searchInputWithSelectedOption) } onAutocompleteOpened(): void { - this.dataSource.setSearchFilter(this.autocompleteSearchFilter$.getValue()) + const searchFilterTokenFragment = this.autocompleteSearchFilter$.getValue() + this.setSearchFilterTokenFragment(searchFilterTokenFragment) + } + + onClearBtnClicked(): void { + this.lastSearchInput$.next('') + + // @ts-ignore + this.inputRef.nativeElement.value = '' + this.formGroup.get('searchControl').reset() } - private parseSearchInput(searchInput: string): SearchTextInput & SearchFiltersInput { + private setSearchFilterTokenFragment({ filterTokenFragmentString, filterTokenKey }: SearchFilterTokenFragment) { + // Apadter LabelData to LabelQuery + const queryParams = ExecutionEventsQuery.toLabelQueryParams(filterTokenFragmentString, filterTokenKey) + this.labelApiService?.fetchList(queryParams) + .pipe(tap((result: string[]) => { + this.autocompleteFilters$.next(result) + })) + .subscribe(() => { + this.autocompleteProcessing$.next(AUTOCOMPLETE_STATE.LOADED) + }, (error) => { + console.log(error) + this.autocompleteProcessing$.next(AUTOCOMPLETE_STATE.ERROR) + }) + + } + + private parseSearchInput(searchInput: string): ParsedSearchInput { // const searchInput = 'is:foo not:"complex foo" another:filter just word with "some brackets" or with :"space" : "spaa as" // good:again'; // const searchInput = 'BOOOO is:foo test1 not:"complex foo" test2 another:filter just word with "some brackets" or with // :"space" : "spaa as" good:again'; - const labelRegexp = /([a-z]+):(\w+|"[\w\s]+")/g - - const parsedItems = searchInput.matchAll(labelRegexp) - let lastProcessedIndex = 0 - let searchString = '' - const searchFiltersDictionary = new Map() - for (const parsedItem of parsedItems) { - const [searchFilter, searchFilterName, rawLabelValue] = parsedItem - const startItemIndex = parsedItem.index + const labelRegexp = /([\w\d]+):(\w+|"[\w\s]+")/g + + // @ts-ignore + const parsedFilterTokens = searchInput.matchAll(labelRegexp) + let lastProcessedTokenIndex = 0 + let searchToken = '' + const parsedFilterTokensDictionary: Map> = new Map() + let isFilterTokensFound = false + for (const parsedToken of parsedFilterTokens) { + isFilterTokensFound = true + const [searchFilter, searchFilterName, rawLabelValue] = parsedToken + const startItemIndex = parsedToken.index const endItemIndex = startItemIndex + searchFilter.length const searchFilterValue = _.trim(rawLabelValue, '"') - searchFiltersDictionary.set(searchFilterName, searchFilterValue) - if (startItemIndex > lastProcessedIndex) { - searchString += - `${ searchString && ' ' }${ searchInput.slice(lastProcessedIndex, startItemIndex).trim().replace(/\s+$/, '') }` + if (!parsedFilterTokensDictionary.has(searchFilterName)) { + parsedFilterTokensDictionary.set(searchFilterName, new Set()) } - lastProcessedIndex = endItemIndex + parsedFilterTokensDictionary.get(searchFilterName).add(searchFilterValue) + if (startItemIndex > lastProcessedTokenIndex) { + searchToken += + `${ searchToken && ' ' }${ searchInput.slice(lastProcessedTokenIndex, startItemIndex).trim().replace(/\s+$/, '') }` + } + lastProcessedTokenIndex = endItemIndex } - + if (!searchToken) { searchToken = searchInput } return Object.assign( - { searchString, autocompleteFilterInput: this.isolationPartialSearchFilter(searchInput) }, - searchFiltersDictionary.size && { searchFilters: searchFiltersDictionary } + { searchToken, searchFilterTokenFragment: this.isolationSearchFilterTokenFragment(searchInput) }, + isFilterTokensFound && { filterTokens: parsedFilterTokensDictionary } ) } - private isolationPartialSearchFilter(searchInput: string): AutocompleteSearchFilter { - const reverseIterableString = this.reverseCharsIterator(searchInput) - let partialSearchFilter = '' - let searchFilterDelimiter = null - let startSearchFilterIndex = reverseIterableString.length - for (const char of reverseIterableString) { - const isCharEqualReservedDelimiters = !!this.filterConfig.filtersTypeByDelimiters[char] - if (isCharEqualReservedDelimiters) { - searchFilterDelimiter = char, startSearchFilterIndex = reverseIterableString.currentIndex + private isolationSearchFilterTokenFragment(searchInput: string): SearchFilterTokenFragment { + // get current filter fragment from searchInput + // const reverseIterableString = this.reverseCharsIterator(searchInput) + let filterTokenFragmentString = '' + let filterTokenFragmentDelimiter = null + let startIndexOfFilterTokenFragment = searchInput.length - 1 + let filterTokenFragmentType + for (; startIndexOfFilterTokenFragment >= 0; startIndexOfFilterTokenFragment--) { + const char = searchInput[startIndexOfFilterTokenFragment] + + if (!startIndexOfFilterTokenFragment) { + filterTokenFragmentString = `${ char }${ filterTokenFragmentString }` + filterTokenFragmentDelimiter = RESERVED_FILTERS_DELIMITERS.EMPTY_STRING + filterTokenFragmentType = 'key' + this.lastFoundFilterTokenKey$.next(filterTokenFragmentString) break } - partialSearchFilter = `${ char }${ partialSearchFilter }` - } - const searchFilterType = this.filterConfig.filtersTypeByDelimiters[searchFilterDelimiter || this.filterConfig.defaultDelimiter] - return { partialSearchFilter, searchFilterDelimiter, searchFilterType, startSearchFilterIndex } - } - private reverseCharsIterator(iterableString: string) { - const _string = iterableString.normalize() - - return { - _string, - length: _string.length, - currentIndex: _string.length, - [Symbol.iterator]() { - this.currentIndex = this.length - return this - }, - next() { - if (this.currentIndex > 0) { - return { done: false, value: this._string.codePointAt(this.currentIndex--) } - } - else { - return { done: true } - } + const isCharEqualReservedDelimiters = RESERVED_FILTERS_DELIMITERS.COLON_STRING === char || + RESERVED_FILTERS_DELIMITERS.SPACE_STRING === char + + if (isCharEqualReservedDelimiters) { + filterTokenFragmentDelimiter = char + filterTokenFragmentType = char === RESERVED_FILTERS_DELIMITERS.COLON_STRING ? 'value' : 'key' + if (filterTokenFragmentType === 'key') { this.lastFoundFilterTokenKey$.next(filterTokenFragmentString) } + break } + + filterTokenFragmentString = `${ char }${ filterTokenFragmentString }` } + return Object.assign({ + filterTokenFragmentString, + filterTokenFragmentDelimiter, + filterTokenFragmentType, + startIndexOfFilterTokenFragment + }, filterTokenFragmentType === 'value' && { filterTokenKey: this.lastFoundFilterTokenKey$.getValue() }) } } diff --git a/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.html b/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.html index caa17ec3..f05c0a30 100644 --- a/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.html +++ b/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.html @@ -13,31 +13,31 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> +
+ +
diff --git a/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.ts b/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.ts index 68ae5f89..5e59f2b3 100644 --- a/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.ts +++ b/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.component.ts @@ -15,9 +15,10 @@ */ import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { FormControl, FormGroup } from '@angular/forms' import { MatAutocomplete } from '@angular/material/autocomplete' import { Subject } from 'rxjs' -import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators' +import { debounceTime, takeUntil } from 'rxjs/operators' import { BaseComponent } from 'spline-utils' @@ -27,57 +28,60 @@ import { BaseComponent } from 'spline-utils' }) export class SplineSearchBoxComponent extends BaseComponent { - @ViewChild('inputRef', { read: ElementRef, static: true }) inputRef: ElementRef + @ViewChild('inputRef') inputRef: ElementRef @Input() placeholder = 'COMMON.SEARCH' @Input() matAutocomplete: MatAutocomplete @Output() search$ = new EventEmitter() @Output() clear$ = new EventEmitter() isFocused = false - inputValue: string + + control = new FormControl() + formGroup = new FormGroup({ + searchControl: this.control + }) readonly emitSearchEventDebounceTimeInUs = 300 protected searchValueChanged$ = new Subject() constructor(private cdr: ChangeDetectorRef) { super() - this.searchValueChanged$ + this.control.valueChanges .pipe( // wait some time between keyUp events debounceTime(this.emitSearchEventDebounceTimeInUs), // emit only different value form the previous one - distinctUntilChanged((a, b) => a.trim().toLocaleLowerCase() === b.trim().toLocaleLowerCase()), + // distinctUntilChanged((a, b) => a.trim().toLocaleLowerCase() === b.trim().toLocaleLowerCase()), takeUntil(this.destroyed$) ) - .subscribe( - value => this.search$.emit(value) + .subscribe(value => { + this.search$.emit(value) + } ) } @Input() set searchTerm(value: string) { - this.inputValue = value - } - - onSearchChanged(searchTerm: string): void { - this.inputValue = searchTerm - this.searchValueChanged$.next(searchTerm) - this.isFocused = true + // @ts-ignore + this.inputRef?.nativeElement?.value = value + this.control.setValue(value) } onClearBtnClicked(): void { - this.onSearchChanged('') - this.focusSearchInput() + // @ts-ignore + this.inputRef.nativeElement.value = '' + this.control.reset() + // this.focusSearchInput() } - focusSearchInput(): void { - this.inputRef.nativeElement.focus() - } + // focusSearchInput(): void { + // this.inputRef.nativeElement.focus() + // } clearFocus(): void { this.isFocused = false } - detectChanges() { - this.cdr.detectChanges() - } + // detectChanges() { + // this.cdr.detectChanges() + // } } diff --git a/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.module.ts b/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.module.ts index da52ce23..5d6c58f5 100644 --- a/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.module.ts +++ b/ui/projects/spline-common/main/src/common/component/search-box/spline-search-box.module.ts @@ -16,7 +16,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' -import { FormsModule } from '@angular/forms' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { MatAutocompleteModule } from '@angular/material/autocomplete' import { MatIconModule } from '@angular/material/icon' import { MatProgressBarModule } from '@angular/material/progress-bar' @@ -29,7 +29,7 @@ import { SplineSearchBoxComponent } from './spline-search-box.component' @NgModule({ declarations: [ - SplineSearchBoxComponent, + SplineSearchBoxComponent ], imports: [ CommonModule, @@ -40,8 +40,9 @@ import { SplineSearchBoxComponent } from './spline-search-box.component' MatAutocompleteModule, MatProgressSpinnerModule, MatProgressBarModule, + ReactiveFormsModule ], - exports: [SplineSearchBoxComponent], + exports: [SplineSearchBoxComponent] }) export class SplineSearchBoxModule { } diff --git a/ui/projects/spline-common/main/styles/spline-common/components/_search-box.component.scss b/ui/projects/spline-common/main/styles/spline-common/components/_search-box.component.scss index 1ab86f61..974e75be 100644 --- a/ui/projects/spline-common/main/styles/spline-common/components/_search-box.component.scss +++ b/ui/projects/spline-common/main/styles/spline-common/components/_search-box.component.scss @@ -137,11 +137,11 @@ $border-radius: $height / 2; } } -spline-search-box[theme='grey'] .spline-search-box { +[theme='grey'] .spline-search-box { @include spline-search-box($color-grey-light, $color-grey-dark-50, $color-grey-dark); } -spline-search-box[theme='white-transparent'] .spline-search-box { +[theme='white-transparent'] .spline-search-box { @include spline-search-box($color-white-25, $color-white-55, $color-grey-dark, $color-grey-light-2); } diff --git a/ui/projects/spline-shared/attributes/src/components/search/spline-attribute-search.component.ts b/ui/projects/spline-shared/attributes/src/components/search/spline-attribute-search.component.ts index f919168a..b37ff66a 100644 --- a/ui/projects/spline-shared/attributes/src/components/search/spline-attribute-search.component.ts +++ b/ui/projects/spline-shared/attributes/src/components/search/spline-attribute-search.component.ts @@ -84,7 +84,6 @@ export class SplineAttributeSearchComponent implements OnDestroy { }) this.searchTerm$.next('') this.splineSearchBoxComponent.clearFocus() - } onAutocompleteOpened(): void { diff --git a/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table-store.ns.ts b/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table-store.ns.ts index 35f8d297..adbd0b9f 100644 --- a/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table-store.ns.ts +++ b/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table-store.ns.ts @@ -44,7 +44,6 @@ export namespace SplineSearchDynamicTableStoreNs { queryParams: Params, queryParamAlias: string ): SearchParams | null { - const urlString = queryParams[queryParamAlias] return urlString ? searchParamsFromUrlString(urlString) diff --git a/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.html b/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.html index 9648fbac..93aea824 100644 --- a/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.html +++ b/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.html @@ -31,7 +31,8 @@

diff --git a/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.ts b/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.ts index 36c46c24..59107db1 100644 --- a/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.ts +++ b/ui/projects/spline-shared/dynamic-table/main/src/components/search-dynamic-table/spline-search-dynamic-table.component.ts @@ -20,6 +20,7 @@ import { ActivatedRoute, NavigationEnd, Params, Router, RouterEvent } from '@ang import { isEqual } from 'lodash-es' import { Observable, Subject } from 'rxjs' import { distinctUntilChanged, filter, first, map, repeatWhen, skip, startWith, takeUntil } from 'rxjs/operators' +import { LabelApiService } from 'spline-api' import { DtCellCustomEvent, DtHeaderCellCustomEvent, @@ -58,7 +59,8 @@ export class SplineSearchDynamicTableComponent constructor(private readonly activatedRoute: ActivatedRoute, - private readonly router: Router + private readonly router: Router, + readonly labelApiService: LabelApiService ) { super() this.updateState( @@ -117,8 +119,24 @@ export class SplineSearchDynamicTableComponent>): string[] { + const result = [] + for (const [filterKey, filterSetOfValues] of filters.entries()) { + const filterValues = [] + for (const filterValue of filterSetOfValues.values()) { + filterValues.push(filterValue) + } + result.push(`${ filterKey }:${ filterValues.join() }`) + } + return result } onRefreshDataClick(): void { diff --git a/ui/projects/spline-shared/dynamic-table/main/src/spline-dynamic-table-shared.module.ts b/ui/projects/spline-shared/dynamic-table/main/src/spline-dynamic-table-shared.module.ts index 7bc94dfd..09679ad6 100644 --- a/ui/projects/spline-shared/dynamic-table/main/src/spline-dynamic-table-shared.module.ts +++ b/ui/projects/spline-shared/dynamic-table/main/src/spline-dynamic-table-shared.module.ts @@ -38,6 +38,7 @@ import { splineDtSharedComponents } from './components' SplineTranslateModule.forChild({}), MatAutocompleteModule ], + providers: [], declarations: [ ...splineDtSharedComponents ], diff --git a/ui/projects/spline-shared/events/src/data-sources/events.factory-store.ts b/ui/projects/spline-shared/events/src/data-sources/events.factory-store.ts index ae223626..93319e0b 100644 --- a/ui/projects/spline-shared/events/src/data-sources/events.factory-store.ts +++ b/ui/projects/spline-shared/events/src/data-sources/events.factory-store.ts @@ -23,7 +23,7 @@ import { ExecutionEventsQuery } from 'spline-api' import { SplineConfigApiService } from 'spline-shared' -import { QuerySorter, SearchFactoryStore, SearchQuery } from 'spline-utils' +import { QuerySorter, SearchDataSourceConfig, SearchFactoryStore, SearchQuery } from 'spline-utils' import SortDir = QuerySorter.SortDir import SearchParams = SearchQuery.SearchParams @@ -50,7 +50,8 @@ export class EventsFactoryStore extends SearchFactoryStore) } protected getDataObserver( diff --git a/ui/projects/spline-shared/main/src/common/store-extras/dynamic-filter.store-extras.ts b/ui/projects/spline-shared/main/src/common/store-extras/dynamic-filter.store-extras.ts index 03ca07c0..ec9ff0eb 100644 --- a/ui/projects/spline-shared/main/src/common/store-extras/dynamic-filter.store-extras.ts +++ b/ui/projects/spline-shared/main/src/common/store-extras/dynamic-filter.store-extras.ts @@ -62,7 +62,6 @@ export namespace DynamicFilterStoreExtras { filter(([queryFilter, currentQueryFilter]) => !isEqual(queryFilter, currentQueryFilter)) ) .subscribe(([queryFilter]) => { - debugger; dataSource.setFilter(queryFilter) }) diff --git a/ui/projects/spline-utils/main/src/common/models/query/query-params.models.ts b/ui/projects/spline-utils/main/src/common/models/query/query-params.models.ts index 6c663168..fb849b2d 100644 --- a/ui/projects/spline-utils/main/src/common/models/query/query-params.models.ts +++ b/ui/projects/spline-utils/main/src/common/models/query/query-params.models.ts @@ -14,7 +14,6 @@ * limitations under the License. */ - import { SplineRecord } from '../heplers' import { QueryPager } from './query-pager.models' @@ -26,3 +25,16 @@ export interface PageQueryParams[] } + +export interface LabelPageQueryParams { + length?: number + offset?: number + search?: string +} + +export interface LabelValuesPageQueryParams extends LabelPageQueryParams { + labelName: string + length?: number + offset?: number + search?: string +} diff --git a/ui/projects/spline-utils/main/src/common/models/search-query/search.factory-store.ts b/ui/projects/spline-utils/main/src/common/models/search-query/search.factory-store.ts index 23449f27..0f2579a4 100644 --- a/ui/projects/spline-utils/main/src/common/models/search-query/search.factory-store.ts +++ b/ui/projects/spline-utils/main/src/common/models/search-query/search.factory-store.ts @@ -50,7 +50,6 @@ export abstract class SearchFactoryStore> = new BehaviorSubject(DEFAULT_RENDER_DATA) readonly searchParams$: BehaviorSubject> = new BehaviorSubject(undefined) - readonly autocompleteFilters$: BehaviorSubject = new BehaviorSubject([]) readonly disconnected$ = new Subject() readonly serverDataUpdates$: Observable @@ -95,36 +94,15 @@ export abstract class SearchFactoryStore[]): void { this.updateSearchParams(this.withResetPagination({ sortBy })) } setFilter(filterValue: TFilter): void { const searchParams = this.withResetPagination({ filter: filterValue }) - debugger; this.updateSearchParams(searchParams) } @@ -168,15 +146,14 @@ export abstract class SearchFactoryStore>): void { - debugger; this.updateSearchParams(searchParams) } connect(collectionViewer: CollectionViewer): Observable { return this.dataState$ .pipe( - takeUntil(this.disconnected$), - map(x => x.data?.items ?? []) + map(x => x.data?.items ?? []), + takeUntil(this.disconnected$) ) } @@ -204,7 +181,6 @@ export abstract class SearchFactoryStore this.updateDataState({ @@ -213,7 +189,6 @@ export abstract class SearchFactoryStore this.getDataObserver(searchParams) .pipe( - takeUntil(this.disconnected$), catchError((error) => { this.updateDataState({ loadingProcessing: ProcessingStoreNs.eventProcessingFinish( @@ -223,7 +198,8 @@ export abstract class SearchFactoryStore { if (result !== null) { diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 5ea61ac2..0f6505a9 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -33,7 +33,6 @@ import { SplineIconModule, SplineSearchBoxModule } from 'spline-common' import { DynamicFilterCoreModule } from 'spline-common/dynamic-filter' import { SplineLayoutModule } from 'spline-common/layout' import { SPLINE_CONFIG_SETTINGS, SplineConfigModule, SplineConfigSettings } from 'spline-shared' -import { SplineAttributesSharedModule } from 'spline-shared/attributes' import { SplineUtilsCommonModule } from 'spline-utils' import { SPLINE_TRANSLATE_COMMON_ASSETS, SplineTranslateCoreModule, SplineTranslateModule, toAssetsFilePath } from 'spline-utils/translate' @@ -46,8 +45,8 @@ import { SplineGlobalErrorHandler } from './services' export const metaReducers: MetaReducer[] = !environment.production - ? [storeFreeze] - : [] + ? [storeFreeze] + : [] @NgModule({ declarations: [ @@ -74,7 +73,6 @@ export const metaReducers: MetaReducer[] = SplineLayoutModule, SplineSearchBoxModule, SplineConfigModule, - SplineAttributesSharedModule, SplineIconModule, SplineUtilsCommonModule, NgxDaterangepickerMd.forRoot(),