@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
849 lines (842 loc) • 56.5 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, Injectable, InjectionToken, EventEmitter, Output, Input, Component, ViewChild, NgModule } from '@angular/core';
import { QueriesUtil, SmartGroupsService } from '@c8y/client';
import * as i2 from '@c8y/ngx-components';
import { FeatureCacheService, ContextRouteService, WILDCARD_SEARCH_FEATURE_KEY, SearchFilters, ViewContext, TabsOutletComponent, TabComponent, C8yTranslatePipe, RouterService, SearchInputComponent, getBasicInputArrayFormFieldConfig, BuiltInActionType, FilteringActionType, DataGridComponent, EmptyStateComponent, UserPreferencesConfigurationStrategy, DATA_GRID_CONFIGURATION_STRATEGY, DATA_GRID_CONFIGURATION_CONTEXT_PROVIDER, Status, TitleComponent, C8yTranslateDirective, CoreModule, CoreSearchModule, hookRoute, hookSearch } from '@c8y/ngx-components';
import { AssetNodeService } from '@c8y/ngx-components/assets-navigator';
import { DeviceGridService, NameDeviceGridColumn, ModelDeviceGridColumn, SerialNumberDeviceGridColumn, RegistrationDateDeviceGridColumn, SystemIdDeviceGridColumn, ImeiDeviceGridColumn, AlarmsDeviceGridColumn, DeviceGridModule } from '@c8y/ngx-components/device-grid';
import { BehaviorSubject, firstValueFrom, catchError, of, lastValueFrom, Subject } from 'rxjs';
import { AssetTypeGridColumn, AssetTypeCellRendererComponent } from '@c8y/ngx-components/data-grid-columns/asset-type';
import { AsyncPipe } from '@angular/common';
import * as i1$1 from '@angular/router';
import { Router, ActivationEnd } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import * as i1 from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { gettext } from '@c8y/ngx-components/gettext';
import { filter, map, startWith, catchError as catchError$1, takeUntil } from 'rxjs/operators';
import { SubAssetsService, DeleteAssetsModalComponent } from '@c8y/ngx-components/sub-assets';
import { BsModalService } from 'ngx-bootstrap/modal';
class AssetSearchService {
constructor() {
this.GRID_CONFIG_STORAGE_KEY = 'search-grid-config';
this.DEFAULT_PAGE_SIZE = 50;
this.getGlobalSearchData = this.getSearchData.bind(this);
this.appliedFilters$ = new BehaviorSubject({
allFilters: true,
onlyDevices: true,
onlyGroupsAndAssets: true,
currentHierarchy: false
});
this.featureCacheService = inject(FeatureCacheService);
this.deviceGridService = inject(DeviceGridService);
this.assetNodeService = inject(AssetNodeService);
this.contextRouteService = inject(ContextRouteService);
this.queriesUtil = new QueriesUtil();
}
/**
* Resets the status of applied filters, used during the search.
* Applies only to filters: 'All', 'Show only devices', 'Show only groups and assets'.
*/
resetAppliedFilters() {
this.appliedFilters$.next({
allFilters: true,
onlyDevices: true,
onlyGroupsAndAssets: true,
currentHierarchy: false
});
}
buildCombinedRootQueryFilter(columns, pagination) {
const rootQuery = {
__filter: {
__and: { __not: { __has: `c8y_IsBinary` } }
}
};
const { onlyDevices, onlyGroupsAndAssets } = this.appliedFilters$.value;
const searchQuery = this.buildSearchQuery({ onlyDevices, onlyGroupsAndAssets });
const userQuery = this.deviceGridService.getQueryObj(columns, pagination);
const queryPart = this.queriesUtil.addOrderbys(rootQuery, userQuery.__orderby, 'append');
const fullQuery = this.queriesUtil.addAndFilter(queryPart, userQuery.__filter);
const queryWithSearch = this.queriesUtil.addAndFilter(fullQuery, searchQuery);
return queryWithSearch;
}
async getData(columns, pagination, text) {
const query = this.buildCombinedRootQueryFilter(columns, pagination);
if (await this.isWildcardSearchEnabled()) {
const wildcardQuery = this.queriesUtil.addAndFilter(query, {
__or: {
name: `*${text.trim().replace(/\s+/g, '*')}*`,
id: text.trim(),
'c8y_Hardware.serialNumber': `*${text.trim().replace(/\s+/g, '*')}*`
}
});
return this.assetNodeService.getAllInventories({
...pagination,
query: this.queriesUtil.buildQuery(wildcardQuery)
});
}
return this.assetNodeService.getAllInventories({
...pagination,
query: this.queriesUtil.buildQuery(query),
text
});
}
getDefaultColumns() {
const defaultColumns = [
new NameDeviceGridColumn({ sortOrder: 'asc' }),
new ModelDeviceGridColumn(),
new SerialNumberDeviceGridColumn({ visible: false }),
new RegistrationDateDeviceGridColumn({ visible: false }),
new SystemIdDeviceGridColumn({ visible: false }),
new ImeiDeviceGridColumn({ visible: false }),
new AlarmsDeviceGridColumn()
];
return defaultColumns;
}
getDefaultActionControls() {
return [];
}
getDefaultBulkActionControls() {
return [];
}
getDefaultPagination() {
return {
pageSize: 25,
currentPage: 1
};
}
async isWildcardSearchEnabled() {
return firstValueFrom(this.featureCacheService
.getFeatureState(WILDCARD_SEARCH_FEATURE_KEY)
.pipe(catchError(() => of(true))));
}
buildSearchQuery(model) {
const filter = {};
const ors = [];
if (model.types?.length) {
ors.push({ type: { __in: model.types } });
}
if (model.onlyDevices) {
ors.push({ __has: 'c8y_IsDevice' });
}
if (model.onlyGroupsAndAssets) {
ors.push({ __has: 'c8y_IsDynamicGroup' });
ors.push({ __has: 'c8y_IsDeviceGroup' });
}
if (!model.onlyDevices && !model.onlyGroupsAndAssets) {
ors.push([
{ __has: 'c8y_IsDeviceGroup' },
{ __has: 'c8y_IsDevice' },
{ __has: 'c8y_IsAsset' }
]);
}
if (ors.length) {
filter.__or = ors;
}
if (model.currentHierarchy && this.contextRouteService.activatedContextData?.contextData?.id) {
filter.__and = [
{ __isinhierarchyof: this.contextRouteService.activatedContextData.contextData.id }
];
}
return filter;
}
/**
* Get search data based on the provided text and pagination.
* @param text The search text.
* @param pagination The pagination options.
* @returns The search results.
*/
async getSearchData(text, pagination = { currentPage: 1, pageSize: this.DEFAULT_PAGE_SIZE }) {
const { onlyDevices, onlyGroupsAndAssets, currentHierarchy } = this.appliedFilters$.value;
const query = this.buildSearchQuery({ onlyDevices, onlyGroupsAndAssets, currentHierarchy });
const queryString = this.queriesUtil.buildQuery(query);
if (await this.isWildcardSearchEnabled()) {
const wildcardQuery = this.queriesUtil.addAndFilter(query, {
__or: {
name: `*${text.trim().replace(/\s+/g, '*')}*`,
id: text.trim(),
'c8y_Hardware.serialNumber': `*${text.trim().replace(/\s+/g, '*')}*`
}
});
return this.assetNodeService.getAllInventories({
...pagination,
query: this.queriesUtil.buildQuery(wildcardQuery)
});
}
return this.assetNodeService.getAllInventories({ ...pagination, query: queryString, text });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetSearchService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetSearchService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetSearchService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
const SEARCH_CONFIG = new InjectionToken('SearchConfig');
class SearchCustomFiltersComponent {
constructor() {
this.useTabs = false;
this.customDataQuery = new EventEmitter();
this.refresh = new EventEmitter();
this.tabNames = {
all: gettext('All'),
devices: gettext('Devices'),
groupsAndAssets: gettext('Groups and assets')
};
this.selectedTab = this.tabNames.all;
this.checkboxesState = [
{
label: gettext('All'),
name: SearchFilters.ALL_FILTERS,
value: true,
indeterminate: false,
isDisabled: true
},
{ label: gettext('Show only devices'), name: SearchFilters.ONLY_DEVICES, value: true },
{
label: gettext('Show only groups and assets'),
name: SearchFilters.ONLY_GROUPS_AND_ASSETS,
value: true
}
];
this.isOnlyHierarchyQuery = false;
this.isOnlyHierarchyQueryChange = new EventEmitter();
this.assetSearchService = inject(AssetSearchService);
this.contextRouteService = inject(ContextRouteService);
this.router = inject(Router);
this.contextRouteData$ = this.router.events.pipe(filter(event => event instanceof ActivationEnd), map((routeData) => {
const data = this.contextRouteService.getContextData(routeData.snapshot);
if (data?.context === ViewContext.Group) {
return data;
}
return null;
}), takeUntilDestroyed());
this.context$ = this.contextRouteData$.pipe(startWith(this.contextRouteService.activatedContextData), takeUntilDestroyed());
}
ngOnInit() {
this.customDataQuery.next(this.assetSearchService.getGlobalSearchData);
}
onCheckboxChange(event, checkbox) {
const { checked } = event.target;
if (checked == undefined) {
return;
}
switch (checkbox.name) {
case SearchFilters.ALL_FILTERS:
this.onSelectAll(checkbox, checked);
break;
case SearchFilters.ONLY_DEVICES:
this.onAllDevices(checkbox, checked);
break;
case SearchFilters.ONLY_GROUPS_AND_ASSETS:
this.onGroupsAndAssets(checkbox, checked);
break;
}
// Handle allFilters checkbox when ONLY_GROUPS_AND_ASSETS and ONLY_DEVICES are selected
if (this.getCheckbox(SearchFilters.ONLY_DEVICES).value &&
this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value) {
Object.assign(this.getCheckbox(SearchFilters.ALL_FILTERS), {
indeterminate: false,
isDisabled: true,
value: true
});
}
this.onSearchFilterChange(this.getCheckbox(SearchFilters.ALL_FILTERS).value, this.getCheckbox(SearchFilters.ONLY_DEVICES).value, this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value);
this.refresh.next(null);
}
onTabChange(tabName = this.tabNames.all) {
this.selectedTab = tabName;
this.onSearchFilterChange(tabName === this.tabNames.all, tabName === this.tabNames.devices, tabName === this.tabNames.groupsAndAssets, this.isOnlyHierarchyQuery);
}
onHierarchyQueryChange(checked) {
this.isOnlyHierarchyQuery = checked;
this.isOnlyHierarchyQueryChange.next(checked);
this.onSearchFilterChange(this.selectedTab === this.tabNames.all, this.selectedTab === this.tabNames.devices, this.selectedTab === this.tabNames.groupsAndAssets, checked);
}
onSearchFilterChange(all, devices, groupsAndAssets, currentHierarchy = false) {
this.assetSearchService.appliedFilters$.next({
[SearchFilters.ALL_FILTERS]: all,
[SearchFilters.ONLY_DEVICES]: devices,
[SearchFilters.ONLY_GROUPS_AND_ASSETS]: groupsAndAssets,
[SearchFilters.CURRENT_HIERARCHY]: currentHierarchy
});
this.refresh.next(null);
}
saveCheckboxValue(checkbox, value) {
checkbox.value = value;
}
onSelectAll(currentCheckbox, checked) {
// Block unchecked state
if (checked) {
this.saveCheckboxValue(currentCheckbox, checked);
}
this.getCheckbox(SearchFilters.ALL_FILTERS).isDisabled = true;
this.getCheckbox(SearchFilters.ONLY_DEVICES).value = true;
this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value = true;
}
onAllDevices(currentCheckbox, checked) {
this.saveCheckboxValue(currentCheckbox, checked);
Object.assign(this.getCheckbox(SearchFilters.ALL_FILTERS), {
indeterminate: true,
isDisabled: false,
value: null
});
this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value = true;
}
onGroupsAndAssets(currentCheckbox, checked) {
this.saveCheckboxValue(currentCheckbox, checked);
Object.assign(this.getCheckbox(SearchFilters.ALL_FILTERS), {
indeterminate: true,
isDisabled: false,
value: null
});
this.getCheckbox(SearchFilters.ONLY_DEVICES).value = true;
}
getCheckbox(checkboxName) {
return this.checkboxesState.find(checkbox => checkbox.name === checkboxName);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchCustomFiltersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: SearchCustomFiltersComponent, isStandalone: true, selector: "c8y-search-custom-filters", inputs: { useTabs: "useTabs", isOnlyHierarchyQuery: "isOnlyHierarchyQuery" }, outputs: { customDataQuery: "customDataQuery", refresh: "refresh", isOnlyHierarchyQueryChange: "isOnlyHierarchyQueryChange" }, ngImport: i0, template: "<div class=\"d-flex gap-16 p-l-4 p-l-24 p-r-24\">\n @if (!useTabs) {\n <div class=\"p-b-8 separator-bottom sticky-top p-t-4\">\n @for (checkbox of checkboxesState; track $index) {\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"checkbox.value\"\n [indeterminate]=\"checkbox.indeterminate\"\n (click)=\"onCheckboxChange($event, checkbox)\"\n [attr.disabled]=\"checkbox.isDisabled ? true : null\"\n />\n <span></span>\n <span>{{ checkbox.label | translate }}</span>\n </label>\n }\n </div>\n }\n</div>\n\n@if (useTabs) {\n <c8y-tabs-outlet\n class=\"elevation-none\"\n outletName=\"searchTabs\"\n orientation=\"horizontal\"\n ></c8y-tabs-outlet>\n}\n\n<c8y-tab\n [icon]=\"'asterisk-key'\"\n [title]=\"'All devices, assets and groups' | translate\"\n [isActive]=\"selectedTab === tabNames.all\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.all | translate\"\n [priority]=\"1000\"\n (onSelect)=\"onTabChange(tabNames.all)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'exchange'\"\n [title]=\"'Devices only' | translate\"\n [isActive]=\"selectedTab === tabNames.devices\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.devices | translate\"\n [priority]=\"750\"\n (onSelect)=\"onTabChange(tabNames.devices)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'c8y-modules'\"\n [title]=\"'Assets and groups only' | translate\"\n [isActive]=\"selectedTab === tabNames.groupsAndAssets\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.groupsAndAssets | translate\"\n [priority]=\"500\"\n (onSelect)=\"onTabChange(tabNames.groupsAndAssets)\"\n></c8y-tab>\n\n@if (useTabs && (context$ | async)?.contextData?.name) {\n <div class=\"p-l-48 p-t-8 d-flex a-i-center\">\n <span translate>Search only in`specified group or asset`</span>\n <span class=\"text-bold p-l-4\">{{ (context$ | async)?.contextData?.name }}</span>\n <label\n class=\"m-l-8 c8y-switch c8y-switch--inline\"\n [title]=\"'Search only within the current group or asset hierarchy' | translate\"\n >\n <input\n [attr.aria-label]=\"'Search only within the current group or asset hierarchy' | translate\"\n type=\"checkbox\"\n [ngModel]=\"isOnlyHierarchyQuery\"\n (ngModelChange)=\"onHierarchyQueryChange($event)\"\n />\n <span></span>\n </label>\n </div>\n}\n", dependencies: [{ kind: "component", type: TabsOutletComponent, selector: "c8y-tabs-outlet,c8y-ui-tabs", inputs: ["tabs", "orientation", "navigatorOpen", "outletName", "context", "openFirstTab", "hasHeader"] }, { kind: "component", type: TabComponent, selector: "c8y-tab", inputs: ["path", "label", "icon", "priority", "orientation", "injector", "tabsOutlet", "isActive", "text", "showAlways"], outputs: ["onSelect"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchCustomFiltersComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-search-custom-filters', imports: [C8yTranslatePipe, TabsOutletComponent, TabComponent, AsyncPipe, FormsModule], template: "<div class=\"d-flex gap-16 p-l-4 p-l-24 p-r-24\">\n @if (!useTabs) {\n <div class=\"p-b-8 separator-bottom sticky-top p-t-4\">\n @for (checkbox of checkboxesState; track $index) {\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"checkbox.value\"\n [indeterminate]=\"checkbox.indeterminate\"\n (click)=\"onCheckboxChange($event, checkbox)\"\n [attr.disabled]=\"checkbox.isDisabled ? true : null\"\n />\n <span></span>\n <span>{{ checkbox.label | translate }}</span>\n </label>\n }\n </div>\n }\n</div>\n\n@if (useTabs) {\n <c8y-tabs-outlet\n class=\"elevation-none\"\n outletName=\"searchTabs\"\n orientation=\"horizontal\"\n ></c8y-tabs-outlet>\n}\n\n<c8y-tab\n [icon]=\"'asterisk-key'\"\n [title]=\"'All devices, assets and groups' | translate\"\n [isActive]=\"selectedTab === tabNames.all\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.all | translate\"\n [priority]=\"1000\"\n (onSelect)=\"onTabChange(tabNames.all)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'exchange'\"\n [title]=\"'Devices only' | translate\"\n [isActive]=\"selectedTab === tabNames.devices\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.devices | translate\"\n [priority]=\"750\"\n (onSelect)=\"onTabChange(tabNames.devices)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'c8y-modules'\"\n [title]=\"'Assets and groups only' | translate\"\n [isActive]=\"selectedTab === tabNames.groupsAndAssets\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.groupsAndAssets | translate\"\n [priority]=\"500\"\n (onSelect)=\"onTabChange(tabNames.groupsAndAssets)\"\n></c8y-tab>\n\n@if (useTabs && (context$ | async)?.contextData?.name) {\n <div class=\"p-l-48 p-t-8 d-flex a-i-center\">\n <span translate>Search only in`specified group or asset`</span>\n <span class=\"text-bold p-l-4\">{{ (context$ | async)?.contextData?.name }}</span>\n <label\n class=\"m-l-8 c8y-switch c8y-switch--inline\"\n [title]=\"'Search only within the current group or asset hierarchy' | translate\"\n >\n <input\n [attr.aria-label]=\"'Search only within the current group or asset hierarchy' | translate\"\n type=\"checkbox\"\n [ngModel]=\"isOnlyHierarchyQuery\"\n (ngModelChange)=\"onHierarchyQueryChange($event)\"\n />\n <span></span>\n </label>\n </div>\n}\n" }]
}], propDecorators: { useTabs: [{
type: Input
}], customDataQuery: [{
type: Output
}], refresh: [{
type: Output
}], isOnlyHierarchyQuery: [{
type: Input
}], isOnlyHierarchyQueryChange: [{
type: Output
}] } });
class SearchActionComponent {
constructor() {
this.typeaheadReload = new EventEmitter();
this.featureCacheService = inject(FeatureCacheService);
this.isWildcardSearchEnabled$ = this.featureCacheService
.getFeatureState(WILDCARD_SEARCH_FEATURE_KEY)
.pipe(catchError(() => of(true)));
this.c8yRouter = inject(RouterService);
this.router = inject(Router);
this.moduleConfig = inject(SEARCH_CONFIG, { optional: true });
this.isOnlyHierarchyQuery = false;
this.showAdvancedFilters = this.moduleConfig?.showAdvancedFilters ?? true;
this.customPlaceholder = this.moduleConfig?.placeholder;
}
triggerDataLoad() {
this.typeaheadReload.next(null);
}
onOpenChange(isOpen) {
if (isOpen) {
this.triggerDataLoad();
}
}
async onSearch(on) {
if (await lastValueFrom(this.isWildcardSearchEnabled$)) {
return;
}
this.navigate(['/assetsearch'], {
queryParams: { search: on },
replaceUrl: true
});
}
onFilter(on) {
this.navigate(['/assetsearch'], {
queryParams: { filter: on },
replaceUrl: true
});
}
onClick(mo) {
this.router.navigateByUrl(this.c8yRouter.getHref(mo, '/'));
}
navigate(commands, extras) {
this.router
.navigateByUrl('/', { skipLocationChange: true })
.then(() => this.router.navigate(commands, extras));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: SearchActionComponent, isStandalone: true, selector: "c8y-search-action", ngImport: i0, template: "<c8y-search-input\n #input\n (filter)=\"onFilter($event)\"\n (search)=\"onSearch($event)\"\n (onClick)=\"onClick($event)\"\n (onOpenToggle)=\"onOpenChange($event)\"\n [enableCustomTemplatePlaceholder]=\"true\"\n [customPlaceholder]=\"customPlaceholder\"\n [customDataQuery]=\"customQuery\"\n [externalTerm]=\"typeaheadReload\"\n [mode]=\"(isWildcardSearchEnabled$ | async) ? 'wildcardsearch' : 'search'\"\n>\n @if (showAdvancedFilters && input.term.length > 0) {\n <c8y-search-custom-filters\n (refresh)=\"triggerDataLoad()\"\n [useTabs]=\"isWildcardSearchEnabled$ | async\"\n [(isOnlyHierarchyQuery)]=\"isOnlyHierarchyQuery\"\n (customDataQuery)=\"customQuery = $event\"\n ></c8y-search-custom-filters>\n }\n</c8y-search-input>\n", dependencies: [{ kind: "component", type: SearchInputComponent, selector: "c8y-search-input", inputs: ["mode", "enableCustomTemplatePlaceholder", "customPlaceholder", "externalTerm", "customDataQuery", "container", "groupsOnly"], outputs: ["filter", "search", "reset", "onClick", "onOpenToggle"] }, { kind: "component", type: SearchCustomFiltersComponent, selector: "c8y-search-custom-filters", inputs: ["useTabs", "isOnlyHierarchyQuery"], outputs: ["customDataQuery", "refresh", "isOnlyHierarchyQueryChange"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchActionComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-search-action', imports: [SearchInputComponent, SearchCustomFiltersComponent, AsyncPipe], template: "<c8y-search-input\n #input\n (filter)=\"onFilter($event)\"\n (search)=\"onSearch($event)\"\n (onClick)=\"onClick($event)\"\n (onOpenToggle)=\"onOpenChange($event)\"\n [enableCustomTemplatePlaceholder]=\"true\"\n [customPlaceholder]=\"customPlaceholder\"\n [customDataQuery]=\"customQuery\"\n [externalTerm]=\"typeaheadReload\"\n [mode]=\"(isWildcardSearchEnabled$ | async) ? 'wildcardsearch' : 'search'\"\n>\n @if (showAdvancedFilters && input.term.length > 0) {\n <c8y-search-custom-filters\n (refresh)=\"triggerDataLoad()\"\n [useTabs]=\"isWildcardSearchEnabled$ | async\"\n [(isOnlyHierarchyQuery)]=\"isOnlyHierarchyQuery\"\n (customDataQuery)=\"customQuery = $event\"\n ></c8y-search-custom-filters>\n }\n</c8y-search-input>\n" }]
}], ctorParameters: () => [] });
class AssetTypeSearchGridColumn extends AssetTypeGridColumn {
constructor(hideExtendedFilters, initialColumnConfig, assetSearchService, customPlaceholder) {
super(initialColumnConfig);
this.filterable = true;
this.filteringConfig = this.getFilteringConfig(hideExtendedFilters, assetSearchService, customPlaceholder);
}
getFilteringConfig(hideExtendedFilters, assetSearchService, customPlaceholder) {
return {
fields: [
...getBasicInputArrayFormFieldConfig({
key: 'types',
label: gettext('Show items with type'),
addText: gettext('Add next`type`'),
tooltip: gettext('Use * as a wildcard character'),
placeholder: customPlaceholder ? customPlaceholder : gettext('building`e.g. house`'),
optional: !hideExtendedFilters
}),
{
key: SearchFilters.ALL_FILTERS,
type: 'checkbox',
hide: hideExtendedFilters,
props: {
indeterminate: false,
disabled: false,
label: gettext('All'),
click: (field, clickEvent) => {
const { checked } = clickEvent.target;
// Handle checked state
if (checked) {
field.form.get(SearchFilters.ONLY_DEVICES).setValue(true);
field.form.get(SearchFilters.ONLY_GROUPS_AND_ASSETS).setValue(true);
// Emit new state
assetSearchService.appliedFilters$.next({
[SearchFilters.ALL_FILTERS]: checked,
[SearchFilters.ONLY_DEVICES]: true,
[SearchFilters.ONLY_GROUPS_AND_ASSETS]: true,
[SearchFilters.CURRENT_HIERARCHY]: null
});
}
}
},
expressionProperties: {
'props.indeterminate': (model, formState, field) => {
// Do nothing
if (field.form.get(SearchFilters.ALL_FILTERS).value === true) {
return;
}
// Set indeterminate state
if (!field.form.get(SearchFilters.ONLY_DEVICES).value ||
!field.form.get(SearchFilters.ONLY_GROUPS_AND_ASSETS).value) {
field.form.get(SearchFilters.ALL_FILTERS).setValue(null);
return true;
}
return false;
},
'props.disabled': (model, formState, field) => {
if (field.form.get(SearchFilters.ALL_FILTERS).value === true) {
return true;
}
return false;
}
},
hooks: {
onInit: field => {
// Get initial state
const { allFilters } = assetSearchService?.appliedFilters$?.value;
field.formControl.setValue(allFilters);
}
}
},
{
key: SearchFilters.ONLY_DEVICES,
type: 'checkbox',
hide: hideExtendedFilters,
props: {
indeterminate: false,
label: gettext('Show only devices'),
click: (field, clickEvent) => {
const oldFilterValue = assetSearchService.appliedFilters$.value;
const { checked } = clickEvent.target;
// Handle checked state
if (checked) {
field.form.get(SearchFilters.ALL_FILTERS).setValue(true);
// Emit new state
assetSearchService.appliedFilters$.next({
...oldFilterValue,
[SearchFilters.ALL_FILTERS]: true,
[SearchFilters.ONLY_DEVICES]: checked
});
return;
}
// Handle unchecked state
field.form.get(SearchFilters.ALL_FILTERS).setValue(null); // Trigger indeterminate state
field.form.get(SearchFilters.ONLY_GROUPS_AND_ASSETS).setValue(true);
// Emit new state
assetSearchService.appliedFilters$.next({
[SearchFilters.ALL_FILTERS]: null,
[SearchFilters.ONLY_GROUPS_AND_ASSETS]: true,
[SearchFilters.ONLY_DEVICES]: checked,
[SearchFilters.CURRENT_HIERARCHY]: null
});
}
},
hooks: {
onInit: field => {
// Get initial state
const { onlyDevices } = assetSearchService?.appliedFilters$?.value;
field.formControl.setValue(onlyDevices);
}
}
},
{
key: SearchFilters.ONLY_GROUPS_AND_ASSETS,
type: 'checkbox',
hide: hideExtendedFilters,
props: {
indeterminate: false,
label: gettext('Show only groups and assets'),
click: (field, clickEvent) => {
const oldFilterValue = assetSearchService.appliedFilters$.value;
const { checked } = clickEvent.target;
// Handle checked state
if (checked) {
field.form.get(SearchFilters.ALL_FILTERS).setValue(true);
// Emit new state
assetSearchService.appliedFilters$.next({
...oldFilterValue,
[SearchFilters.ALL_FILTERS]: true,
[SearchFilters.ONLY_GROUPS_AND_ASSETS]: checked
});
return;
}
// Handle unchecked state
field.form.get(SearchFilters.ALL_FILTERS).setValue(null); // Trigger indeterminate state
field.form.get(SearchFilters.ONLY_DEVICES).setValue(true);
// Emit new state
assetSearchService.appliedFilters$.next({
[SearchFilters.ALL_FILTERS]: null,
[SearchFilters.ONLY_GROUPS_AND_ASSETS]: checked,
[SearchFilters.ONLY_DEVICES]: true,
[SearchFilters.CURRENT_HIERARCHY]: false
});
}
},
hooks: {
onInit: field => {
// Get initial state
const { onlyGroupsAndAssets } = assetSearchService?.appliedFilters$?.value;
field.formControl.setValue(onlyGroupsAndAssets);
}
}
}
],
/**
* Adding devices and groups to a filter is already handled in {@link AssetSearchService#buildSearchQuery}
* */
getFilter(model) {
const filter = {};
const ors = [];
if (model.types?.length) {
ors.push({ type: { __in: model.types } });
}
if (ors.length) {
filter.__or = ors;
}
return filter;
}
};
}
}
class SearchGridComponent {
constructor() {
this.title = '';
this.loadingItemsLabel = gettext('Loading results…');
this.selectable = false;
this.onColumnsChange = new EventEmitter();
this.searchText = '';
this.moduleConfig = inject(SEARCH_CONFIG, { optional: true });
this.showAdvancedFilters = this.moduleConfig?.showAdvancedFilters ?? false;
this.customPlaceholder = this.moduleConfig?.placeholder ?? undefined;
this.assetSearchService = inject(AssetSearchService);
this.pagination = this.assetSearchService.getDefaultPagination();
this.bulkActionControls = this.assetSearchService.getDefaultBulkActionControls();
this.refresh = new EventEmitter();
this.featureCacheService = inject(FeatureCacheService);
this.isWildcardSearchEnabled$ = this.featureCacheService
.getFeatureState(WILDCARD_SEARCH_FEATURE_KEY)
.pipe(catchError(() => of(true)));
this.wildcardSearchTitle = gettext('Asset data');
this.fullTextSearchTitle = gettext('Search results');
this.sizeCount = 0;
this.bsModalService = inject(BsModalService);
this.smartGroupsService = inject(SmartGroupsService);
this.subAssetsGridService = inject(SubAssetsService);
}
set _columns(value) {
if (value) {
this.columns = value;
}
else {
this.columns = this.assetSearchService.getDefaultColumns();
}
}
set _pagination(value) {
if (value) {
this.pagination = value;
}
}
set _actionControls(value) {
if (value) {
this.actionControls = value;
}
else {
this.actionControls = this.assetSearchService.getDefaultActionControls();
}
}
set _bulkActionControls(value) {
if (value) {
this.bulkActionControls = value;
}
else {
this.bulkActionControls = this.assetSearchService.getDefaultBulkActionControls();
}
}
getGridConfigContext() {
return {
key: this.columnsConfigKey || this.assetSearchService.GRID_CONFIG_STORAGE_KEY,
configFilter: { filter: false, sort: false }
};
}
ngOnInit() {
if (!this.filteringName) {
this.columns = [
new AssetTypeSearchGridColumn(this.showAdvancedFilters, { sortOrder: 'desc' }, this.assetSearchService, this.customPlaceholder),
...this.assetSearchService.getDefaultColumns()
];
}
else {
this.columns = [
new AssetTypeSearchGridColumn(this.showAdvancedFilters, { sortOrder: 'desc' }, this.assetSearchService),
new NameDeviceGridColumn({
sortOrder: 'asc',
filter: { externalFilterQuery: { names: [this.filteringName] } }
}),
new ModelDeviceGridColumn(),
new SerialNumberDeviceGridColumn({ visible: false }),
new RegistrationDateDeviceGridColumn({ visible: false }),
new SystemIdDeviceGridColumn({ visible: false }),
new ImeiDeviceGridColumn({ visible: false }),
new AlarmsDeviceGridColumn()
];
}
this.serverSideDataCallback = this.onDataSourceModifier.bind(this);
this.setActionControls();
}
ngAfterViewInit() {
this.setInitialFilterForTypeColumn();
}
trackByName(_index, column) {
return column.name;
}
async onDataSourceModifier(dataSourceModifier) {
const response = await this.assetSearchService.getData(dataSourceModifier.columns, dataSourceModifier.pagination, dataSourceModifier.searchText);
const { res, data, paging } = response;
if (paging.currentPage === 1) {
this.sizeCount = 0;
}
this.sizeCount += data.length;
this.onColumnsChange.emit(dataSourceModifier.columns);
return {
res,
data,
paging,
filteredSize: this.sizeCount,
size: undefined
};
}
setActionControls() {
const actionControls = [];
const deleteAction = {
type: BuiltInActionType.Delete,
callback: (asset) => this.onDeleteAsset(asset, this.parentGroup)
};
actionControls.push(deleteAction);
if (!this.actionControls) {
this.actionControls = actionControls;
}
}
updateFiltering(columnNames, action) {
const { type } = action;
if (type === FilteringActionType.ResetFilter) {
this.dataGrid.clearFilters();
}
else {
/**
* TODO: find better solution. After new changes from DM team, we're running into race condition where
* this.dataGrid.updateFiltering is executed before this.configurationStrategy.getConfig$() value is emitted.
* Columns setter sets columns after this.dataGrid.updateFiltering executes its logic. Value of this.columns in
* dataGrid.updateFiltering is just not yet set.
*/
setTimeout(() => {
this.dataGrid.updateFiltering(columnNames, action, false);
}, 500);
}
}
onColumnFilterReset(column) {
if (column.name === 'type') {
this.assetSearchService.resetAppliedFilters();
}
}
onDeleteAsset(asset, parentRef) {
const initialState = {
showWithDeviceUserCheckbox: this.subAssetsGridService.shouldShowWithDeviceUserCheckbox(asset),
asset,
showWithCascadeCheckbox: !this.smartGroupsService.isSmartGroup(asset)
};
const modalRef = this.bsModalService.show(DeleteAssetsModalComponent, { initialState });
modalRef.content.closeSubject.subscribe(async (result) => {
if (result) {
await this.subAssetsGridService.deleteAsset(asset, parentRef, result);
this.refresh.emit();
}
});
}
setInitialFilterForTypeColumn() {
const checkboxes = this.assetSearchService.appliedFilters$.value;
// Set filter only when all checkboxes are not selected
if (checkboxes[SearchFilters.ONLY_DEVICES] !== checkboxes[SearchFilters.ONLY_GROUPS_AND_ASSETS]) {
const externalFilterQuery = {
[SearchFilters.ONLY_DEVICES]: checkboxes[SearchFilters.ONLY_DEVICES],
[SearchFilters.ONLY_GROUPS_AND_ASSETS]: checkboxes[SearchFilters.ONLY_GROUPS_AND_ASSETS]
};
this.updateFiltering(['type'], {
type: FilteringActionType.ApplyFilter,
payload: {
filteringModifier: {
externalFilterQuery
}
}
});
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: SearchGridComponent, isStandalone: true, selector: "c8y-search-grid", inputs: { parentGroup: ["parent-group", "parentGroup"], title: "title", loadingItemsLabel: "loadingItemsLabel", _columns: ["columns", "_columns"], _pagination: ["pagination", "_pagination"], _actionControls: ["actionControls", "_actionControls"], selectable: "selectable", _bulkActionControls: ["bulkActionControls", "_bulkActionControls"], searchText: "searchText", filteringName: "filteringName", columnsConfigKey: "columnsConfigKey" }, outputs: { onColumnsChange: "onColumnsChange" }, providers: [
{
provide: DATA_GRID_CONFIGURATION_STRATEGY,
useClass: UserPreferencesConfigurationStrategy
},
{
provide: DATA_GRID_CONFIGURATION_CONTEXT_PROVIDER,
useExisting: SearchGridComponent
}
], viewQueries: [{ propertyName: "dataGrid", first: true, predicate: DataGridComponent, descendants: true, static: true }], ngImport: i0, template: "<div class=\"card--grid--fullpage border-top border-bottom\">\n <c8y-data-grid\n [title]=\"\n ((isWildcardSearchEnabled$ | async) ? wildcardSearchTitle : fullTextSearchTitle) | translate\n \"\n [loadingItemsLabel]=\"loadingItemsLabel\"\n [columns]=\"columns\"\n [pagination]=\"pagination\"\n [actionControls]=\"actionControls\"\n [selectable]=\"selectable\"\n [bulkActionControls]=\"bulkActionControls\"\n [serverSideDataCallback]=\"serverSideDataCallback\"\n [infiniteScroll]=\"'auto'\"\n [showSearch]=\"true\"\n [searchText]=\"searchText\"\n [refresh]=\"refresh\"\n (onColumnFilterReset)=\"onColumnFilterReset($event)\"\n >\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No results to display.' | translate\"\n [subtitle]=\"'Refine your search terms or check your spelling.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </c8y-data-grid>\n</div>\n", dependencies: [{ kind: "component", type: DataGridComponent, selector: "c8y-data-grid", inputs: ["title", "loadMoreItemsLabel", "loadingItemsLabel", "showSearch", "refresh", "loading", "columns", "rows", "pagination", "childNodePagination", "infiniteScroll", "serverSideDataCallback", "selectable", "singleSelection", "selectionPrimaryKey", "displayOptions", "actionControls", "bulkActionControls", "headerActionControls", "searchText", "configureColumnsEnabled", "showCounterWarning", "activeClassName", "expandableRows", "treeGrid", "hideReload", "childNodesProperty", "parentNodeLabelProperty"], outputs: ["rowMouseOver", "rowMouseLeave", "rowClick", "onConfigChange", "onBeforeFilter", "onBeforeSearch", "onFilter", "itemsSelect", "onReload", "onAddCustomColumn", "onRemoveCustomColumn", "onColumnFilterReset", "onSort", "onPageSizeChange", "onColumnReordered", "onColumnVisibilityChange"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchGridComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-search-grid', providers: [
{
provide: DATA_GRID_CONFIGURATION_STRATEGY,
useClass: UserPreferencesConfigurationStrategy
},
{
provide: DATA_GRID_CONFIGURATION_CONTEXT_PROVIDER,
useExisting: SearchGridComponent
}
], imports: [DataGridComponent, EmptyStateComponent, C8yTranslatePipe, AsyncPipe], template: "<div class=\"card--grid--fullpage border-top border-bottom\">\n <c8y-data-grid\n [title]=\"\n ((isWildcardSearchEnabled$ | async) ? wildcardSearchTitle : fullTextSearchTitle) | translate\n \"\n [loadingItemsLabel]=\"loadingItemsLabel\"\n [columns]=\"columns\"\n [pagination]=\"pagination\"\n [actionControls]=\"actionControls\"\n [selectable]=\"selectable\"\n [bulkActionControls]=\"bulkActionControls\"\n [serverSideDataCallback]=\"serverSideDataCallback\"\n [infiniteScroll]=\"'auto'\"\n [showSearch]=\"true\"\n [searchText]=\"searchText\"\n [refresh]=\"refresh\"\n (onColumnFilterReset)=\"onColumnFilterReset($event)\"\n >\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No results to display.' | translate\"\n [subtitle]=\"'Refine your search terms or check your spelling.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </c8y-data-grid>\n</div>\n" }]
}], propDecorators: { parentGroup: [{
type: Input,
args: ['parent-group']
}], title: [{
type: Input
}], loadingItemsLabel: [{
type: Input
}], _columns: [{
type: Input,
args: ['columns']
}], _pagination: [{
type: Input,
args: ['pagination']
}], _actionControls: [{
type: Input,
args: ['actionControls']
}], selectable: [{
type: Input
}], _bulkActionControls: [{
type: Input,
args: ['bulkActionControls']
}], onColumnsChange: [{
type: Output
}], searchText: [{
type: Input
}], filteringName: [{
type: Input
}], columnsConfigKey: [{
type: Input
}], dataGrid: [{
type: ViewChild,
args: [DataGridComponent, { static: true }]
}] } });
class SearchResultsComponent {
constructor(route, alert) {
this.route = route;
this.alert = alert;
this.filter = '';
this.searchText = '';
this.featureCacheService = inject(FeatureCacheService);
this.isWildcardSearchEnabled$ = this.featureCacheService
.getFeatureState(WILDCARD_SEARCH_FEATURE_KEY)
.pipe(catchError$1(() => of(true)));
this.WARNING_TIMEOUT_TIME = 3000;
this.unsubscribe$ = new Subject();
}
ngOnInit() {
this.route.queryParams.pipe(takeUntil(this.unsubscribe$)).subscribe(params => {
if (params.filter) {
this.filteringName = params.filter;
}
});
}
ngAfterViewInit() {
this.route.queryParams
.pipe(takeUntil(this.unsubscribe$))
.subscribe(({ filter, search }) => this.onQueryParamsChange(filter, search));
}
resetSearch() {
if (this.searchGrid.dataGrid.searchText) {
this.alert.add({
text: gettext('Search reset. Full text search does not support filtering.'),
type: Status.WARNING,
timeout: this.WARNING_TIMEOUT_TIME
});
this.searchText = '';
this.searchGrid.dataGrid.searchText = '';
}
}
resetFilter() {
this.filter = '';
if (this.searchGrid.dataGrid.filteringApplied) {
this.alert.add({
text: gettext('Filter reset. Full text search does not support filtering.'),
type: Status.WARNING,
timeout: this.WARNING_TIMEOUT_TIME
});
this.searchGrid.dataGrid.clearFilters(false);
}
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
onQueryParamsChange(filter, searchTerm) {
if (!this.shouldFilter(filter) && searchTerm) {
this.searchText = searchTerm || '';
}
}
shouldFilter(filter) {
if (!filter) {
return false;
}
this.resetSearch();
this.filter = filter || '';
this.searchGrid.updateFiltering(['name'], {
type: FilteringActionType.ApplyFilter,
payload: {
filteringModifier: {
externalFilterQuery: {
names: [this.filter]
}
}
}
});
return true;
}
static {