blob: 4c160596018ed03320a91613d42c023f61047ec3 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
import {Component, ViewChild, OnInit, OnDestroy} from '@angular/core';
import { combineLatest, forkJoin, of, Subscription } from 'rxjs';
import {defaultIfEmpty, mergeMap, tap} from 'rxjs/operators';
import {ModalDirective} from 'ngx-bootstrap';
import {AccordionPanelComponent} from 'ngx-bootstrap/accordion';
import { TranslateService } from '@ngx-translate/core';
import {MenuItem, SelectItem} from 'primeng/primeng';
import {classToClass, serialize, deserialize} from 'class-transformer';
import { MDMNotificationService } from '../core/mdm-notification.service';
import { OverwriteDialogComponent } from '../core/overwrite-dialog.component';
import { streamTranslate, TRANSLATE } from '../core/mdm-core.module';
import { MDMItem } from '@core/mdm-item';
import {SearchService, SearchDefinition, SearchAttribute, SearchLayout} from './search.service';
import {FilterService, SearchFilter, Condition, Operator} from './filter.service';
import { NavigatorService } from '@navigator/navigator.service';
import { NodeService} from '../navigator/node.service';
import { Node } from '../navigator/node';
import { BasketService} from '../basket/basket.service';
import { QueryService, Row, SearchResult } from '../tableview/query.service';
import { TableviewComponent } from '../tableview/tableview.component';
import { ViewComponent } from '../tableview/view.component';
import { EditSearchFieldsComponent } from './edit-searchFields.component';
@Component({
selector: 'mdm-search',
templateUrl: 'mdm-search.component.html',
})
export class MDMSearchComponent implements OnInit, OnDestroy {
maxResults = 1000;
filters: SearchFilter[] = [];
currentFilter: SearchFilter;
filterName = '';
environments: Node[];
selectedEnvironments: Node[] = [];
definitions: SearchDefinition[] = [];
results: SearchResult = new SearchResult();
allSearchAttributes: { [type: string]: { [env: string]: SearchAttribute[] }} = {};
allSearchAttributesForCurrentResultType: { [env: string]: SearchAttribute[] } = {};
isAdvancedSearchOpen = false;
isAdvancedSearchActive = true;
isSearchResultsOpen = false;
layout: SearchLayout = new SearchLayout;
public dropdownModel: SelectItem[] = [];
public selectedEnvs: string[] = [];
searchFields: { group: string, attribute: string }[] = [];
searchExecuted = false;
selectedRow: SearchFilter;
lazySelectedRow: SearchFilter;
loading = false;
contextMenuItems: MenuItem[] = [
{ label: 'Add to shopping basket', icon: 'fa fa-shopping-cart', command: (event) => this.addSelectionToBasket() }
];
public isSearchLinkedToNavigator: false;
private linkedToNavigatorSub = new Subscription();
@ViewChild(ViewComponent)
viewComponent: ViewComponent;
@ViewChild(TableviewComponent)
tableViewComponent: TableviewComponent;
@ViewChild('lgSaveModal')
childSaveModal: ModalDirective;
@ViewChild(EditSearchFieldsComponent)
editSearchFieldsComponent: EditSearchFieldsComponent;
@ViewChild(OverwriteDialogComponent)
overwriteDialogComponent: OverwriteDialogComponent;
@ViewChild('advancedSearch')
advancedSearchPanel: AccordionPanelComponent;
@ViewChild('searchResults')
searchResultsPanel: AccordionPanelComponent;
constructor(private searchService: SearchService,
private queryService: QueryService,
private filterService: FilterService,
private nodeService: NodeService,
private notificationService: MDMNotificationService,
private basketService: BasketService,
private translateService: TranslateService,
private navigatorService: NavigatorService) { }
ngOnInit() {
this.currentFilter = this.filterService.EMPTY_FILTER;
streamTranslate(this.translateService, TRANSLATE('search.mdm-search.add-to-shopping-basket')).subscribe(
(msg: string) => this.contextMenuItems[0].label = msg);
this.nodeService.getNodes().pipe(
mergeMap(envs => combineLatest([
of(envs),
this.searchService.loadSearchAttributesStructured(envs.map(env => env.sourceName)),
this.filterService.getFilters().pipe(defaultIfEmpty([this.currentFilter])),
this.searchService.getDefinitionsSimple()
]))).subscribe(
([envs, attrs, filters, definitions]) => this.init(envs, attrs, filters, definitions),
error => this.notificationService.notifyError(
this.translateService.instant('search.mdm-search.err-cannot-load-data-sources'), error)
);
// event handlers
this.viewComponent.viewChanged$.subscribe(
() => this.onViewChanged(),
error => this.notificationService.notifyError(this.translateService.instant('search.mdm-search.err-cannot-update-view'), error)
);
}
ngOnDestroy() {
this.saveState();
this.linkedToNavigatorSub.unsubscribe();
}
init(envs: Node[], attrs: { [type: string]: { [env: string]: SearchAttribute[] }},
filters: SearchFilter[], definitions: SearchDefinition[]) {
this.environments = envs;
this.allSearchAttributes = attrs;
this.filters = filters;
this.definitions = definitions;
this.dropdownModel = envs.map(env => <SelectItem>{ value: env.sourceName, label: env.name });
this.selectedEnvs = envs.map(env => env.sourceName);
this.updateSearchAttributesForCurrentResultType();
this.selectedEnvironmentsChanged();
this.loadState();
}
loadState() {
this.results = deserialize(SearchResult, sessionStorage.getItem('mdm-search.searchResult')) || new SearchResult();
this.selectFilter(deserialize(SearchFilter, sessionStorage.getItem('mdm-search.currentFilter')) || this.filterService.EMPTY_FILTER);
this.isAdvancedSearchActive = !('false' === sessionStorage.getItem('mdm-search.isAdvancedSearchActive'));
this.advancedSearchPanel.isOpen = !('false' === sessionStorage.getItem('mdm-search.isAdvancedSearchOpen'));
this.searchResultsPanel.isOpen = !('false' === sessionStorage.getItem('mdm-search.isSearchResultsOpen'));
}
saveState() {
sessionStorage.setItem('mdm-search.isSearchResultsOpen', serialize(this.searchResultsPanel.isOpen));
sessionStorage.setItem('mdm-search.isAdvancedSearchOpen', serialize(this.advancedSearchPanel.isOpen));
sessionStorage.setItem('mdm-search.currentFilter', serialize(this.currentFilter));
sessionStorage.setItem('mdm-search.searchResult', serialize(this.results));
sessionStorage.setItem('mdm-search.isAdvancedSearchActive', this.isAdvancedSearchActive.toString());
}
onViewClick(e: Event) {
e.stopPropagation();
}
onCheckBoxClick(event: any) {
event.stopPropagation();
this.isAdvancedSearchActive = event.target.checked;
}
onViewChanged() {
if (this.searchExecuted) {
this.onSearch();
}
}
selectedEnvironmentsChanged() {
this.currentFilter.environments = this.selectedEnvs;
if (this.environments) {
let envs = this.environments.filter(env =>
this.currentFilter.environments.find(envName => envName === env.sourceName));
if (envs.length === 0) {
this.selectedEnvironments = this.environments;
} else {
this.selectedEnvironments = envs;
}
}
this.calcCurrentSearch();
}
loadFilters() {
this.filters = [];
this.filterService.getFilters().pipe(
defaultIfEmpty([this.currentFilter]))
.subscribe(
filters => this.filters = this.filters.concat(filters),
error => this.notificationService.notifyError(
this.translateService.instant('search.mdm-search.err-cannot-load-search-filter'), error)
);
}
selectFilterByName(defaultFilterName: string) {
this.selectFilter(this.filters.find(f => f.name === defaultFilterName));
}
removeSearchField(searchField: { group: string, attribute: string }) {
let index = this.searchFields.findIndex(sf => sf.group === searchField.group && sf.attribute === searchField.attribute);
this.searchFields.splice(index, 1);
}
onResultTypeChanged(event: {value: string, originalEvent: Event}) {
const searchDef = this.definitions.find(sd => sd.value === event.value);
if (searchDef) {
this.currentFilter.resultType = searchDef.type;
this.updateSearchAttributesForCurrentResultType();
}
}
updateSearchAttributesForCurrentResultType() {
if (this.allSearchAttributes.hasOwnProperty(this.getSelectedDefinition())) {
this.allSearchAttributesForCurrentResultType = this.allSearchAttributes[this.getSelectedDefinition()];
}
}
getSearchDefinition(type: string) {
return this.definitions.find(def => def.type === type);
}
getSelectedDefinition() {
let def = this.getSearchDefinition(this.currentFilter.resultType);
if (def) {
return def.value;
}
}
onSearch() {
let query;
this.loading = true;
this.isSearchResultsOpen = true;
if (this.isAdvancedSearchActive) {
query = this.searchService.convertToQuery(this.currentFilter, this.allSearchAttributes, this.viewComponent.selectedView);
} else {
let filter = classToClass(this.currentFilter);
filter.conditions = [];
query = this.searchService.convertToQuery(filter, this.allSearchAttributes, this.viewComponent.selectedView);
}
this.queryService.query(query).pipe(
tap(result => this.generateWarningsIfMaxResultsReached(result)))
.subscribe(
result => {
this.results = result;
this.isSearchResultsOpen = true;
this.searchExecuted = true;
this.loading = false;
},
error => this.notificationService.notifyError(
this.translateService.instant('search.mdm-search.err-cannot-process-search-query'), error)
);
}
generateWarningsIfMaxResultsReached(result: SearchResult) {
let resultsPerSource = result.rows
.map(r => r.source)
.reduce((prev, item) => { (prev[item]) ? prev[item] += 1 : prev[item] = 1; return prev; }, {});
Object.keys(resultsPerSource)
.filter(source => resultsPerSource[source] > this.maxResults)
.forEach(source => this.notificationService.notifyWarn(
this.translateService.instant('search.mdm-search.errheading-too-many-search-results'),
this.translateService.instant('search.mdm-search.err-too-many-search-results', {'numresults': this.maxResults, 'source': source})));
}
calcCurrentSearch() {
let environments = this.currentFilter.environments;
let conditions = this.currentFilter.conditions;
let type = this.getSelectedDefinition();
this.layout = SearchLayout.createSearchLayout(environments, this.allSearchAttributesForCurrentResultType, conditions);
}
onFilterChange(e: any) {
this.selectFilter(e.value);
}
selectFilter(filter: SearchFilter) {
this.currentFilter = classToClass(filter);
this.selectedEnvs = this.currentFilter.environments;
this.updateSearchAttributesForCurrentResultType();
this.selectedEnvironmentsChanged();
this.calcCurrentSearch();
}
resetConditions(e: Event) {
e.stopPropagation();
this.currentFilter.conditions.forEach(cond => cond.value = []);
this.selectFilter(this.currentFilter);
}
clearResultlist(e: Event) {
e.stopPropagation();
this.results = new SearchResult();
}
deleteFilter(e: Event) {
e.stopPropagation();
if (this.currentFilter.name === this.filterService.NO_FILTER_NAME
|| this.currentFilter.name === this.filterService.NEW_FILTER_NAME) {
this.notificationService
.notifyInfo(this.translateService.instant('search.mdm-search.errheading-cannot-delete-search-filter-none-selected'),
this.translateService.instant('search.mdm-search.err-cannot-delete-search-filter-none-selected'));
} else {
this.layout = new SearchLayout;
this.filterService.deleteFilter(this.currentFilter.name).subscribe(
() => {
this.loadFilters();
this.selectFilter(new SearchFilter(this.filterService.NO_FILTER_NAME, [], 'Test', '', []));
},
error => this.notificationService.notifyError(
this.translateService.instant('search.mdm-search.err-cannot-delete-search-filter'), error)
);
}
}
saveFilter(e: Event) {
e.stopPropagation();
if (this.filters.find(f => f.name === this.filterName) != undefined) {
this.childSaveModal.hide();
this.overwriteDialogComponent.showOverwriteModal(this.translateService.instant('search.mdm-search.a-filter')).subscribe(
needSave => this.saveFilter2(needSave),
error => {
this.saveFilter2(false);
this.notificationService.notifyError(this.translateService.instant('search.mdm-search.err-cannot-save-search-filter'), error);
}
);
} else {
this.saveFilter2(true);
}
}
saveFilter2(save: boolean) {
if (save) {
let filter = this.currentFilter;
filter.name = this.filterName;
this.filterService.saveFilter(filter).subscribe(
() => {
this.loadFilters();
this.selectFilter(filter);
},
error => this.notificationService.notifyError(
this.translateService.instant('search.mdm-search.err-cannot-save-search-filter'), error)
);
this.childSaveModal.hide();
} else {
this.childSaveModal.show();
}
}
removeCondition(condition: Condition) {
this.currentFilter.conditions = this.currentFilter.conditions
.filter(c => !(c.type === condition.type && c.attribute === condition.attribute));
this.calcCurrentSearch();
}
selected2Basket(e: Event) {
e.stopPropagation();
this.tableViewComponent.selectedViewRows.forEach(row => this.basketService.add(Row.getItem(row)));
}
showSaveModal(e: Event) {
e.stopPropagation();
if (this.currentFilter.name === this.filterService.NO_FILTER_NAME
|| this.currentFilter.name === this.filterService.NEW_FILTER_NAME) {
this.filterName = '';
} else {
this.filterName = this.currentFilter.name;
}
this.childSaveModal.show();
}
showSearchFieldsEditor(e: Event, conditions?: Condition[]) {
e.stopPropagation();
this.editSearchFieldsComponent.show(conditions).subscribe(
conds => {
if (!conditions) {
let filter = new SearchFilter(this.filterService.NEW_FILTER_NAME, this.currentFilter.environments, 'Test', '', conds);
this.selectFilter(filter);
}
this.currentFilter.conditions = conds;
this.calcCurrentSearch();
},
error => this.notificationService.notifyError(
this.translateService.instant('search.mdm-search.err-cannot-display-search-field-editor'), error)
);
}
addSelectionToBasket() {
this.basketService.add(Row.getItem(this.tableViewComponent.menuSelectedRow));
}
mapSourceNameToName(sourceName: string) {
return NodeService.mapSourceNameToName(this.environments, sourceName);
}
getSaveFilterBtnTitle () {
return this.filterName
? TRANSLATE('search.mdm-search.tooltip-save-search-filter')
: TRANSLATE('search.mdm-search.tooltip-no-name-set') ;
}
getAdvancedSearchCbxTitle() {
return this.isAdvancedSearchActive
? TRANSLATE('search.mdm-search.tooltip-disable-advanced-search')
: TRANSLATE('search.mdm-search.tooltip-enable-advanced-search');
}
onRowSelect(e: any) {
if (this.lazySelectedRow !== e.data) {
this.selectedRow = e.data;
this.filterName = e.data.name;
} else {
this.selectedRow = undefined;
this.filterName = '';
}
this.lazySelectedRow = this.selectedRow;
}
public onToggleSearchLinkedToNavigator(event: {checked: boolean, originalEvent: MouseEvent}) {
// prevent advanced-search-tab to toggle.
event.originalEvent.stopPropagation();
if(event.checked) {
this.linkedToNavigatorSub.add(this.navigatorService.onSelectionChanged().pipe(tap(obj => this.handleSelectedTreeNodeChanged(obj))).subscribe());
} else {
this.linkedToNavigatorSub.unsubscribe();
this.unlinkConditions();
}
}
private handleSelectedTreeNodeChanged(object: Node | MDMItem) {
// unlink other conditions (the ones that where not created via link but got linked)
this.unlinkConditions();
if (object != undefined) {
// remove other conditions created via link
const oldIndex = this.currentFilter.conditions.findIndex(cond => cond.isCreatedViaLink);
if (oldIndex > -1) {
this.currentFilter.conditions.splice(oldIndex, 1);
}
// add conditions and recalculate search
this.addLinkedConditions(object).pipe(tap(() => this.calcCurrentSearch())).subscribe();
}
}
/**
* Links the related condition or create linked condition if none exists.
* @param object is Node if selected TreeNode is real node, is MDMItem if selected TreeNode is virtual node.
*/
private addLinkedConditions(object: Node | MDMItem) {
let response = of(undefined);
if (object.type != 'Environment') {
if (object instanceof Node) {
this.addLinkedConditionForNode(object);
} else if (object.filter) {
response = this.addLinkedConditionsforVirtualNode(object);
}
}
return response;
}
/**
* Converts Item filter into Conditions via REST-Service. The Conditions are set to the
* linked condition or added as new if none exists
* @param item the virtual Node
*/
private addLinkedConditionsforVirtualNode(item: MDMItem) {
return this.searchService.convertToCondition(item.source, item.filter).pipe(tap(conditions =>
conditions.forEach(condition => {
condition.isCurrentlyLinked = true
let linkedIndex = this.currentFilter.conditions.findIndex(cond => cond.type === condition.type && cond.attribute === condition.attribute);
if (linkedIndex > -1) {
this.currentFilter.conditions.splice(linkedIndex, 1, condition);
} else {
condition.isCreatedViaLink = true;
this.currentFilter.conditions.push(condition);
}
})));
}
/**
* Links the related condition or create linked condition if none exists.
* @param node the Node
*/
private addLinkedConditionForNode(node: Node) {
const linkedCondition = this.currentFilter.conditions.find(cond => cond.type === node.type && cond.attribute === 'Id');
if (linkedCondition) {
linkedCondition.value = [node.id];
linkedCondition.isCurrentlyLinked = true;
} else {
this.currentFilter.conditions.push(new Condition(node.type, 'Id', Operator.EQUALS, [node.id], 'string', true, true));
}
}
private unlinkConditions() {
this.currentFilter.conditions.forEach(cond => cond.isCurrentlyLinked = false);
}
}