UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,114 lines (1,102 loc) 114 kB
import * as i1 from '@c8y/ngx-components'; import { NavigatorNode, gettext, DeviceStatusComponent, GroupFragment, ModalService, AlertService, BreadcrumbService, AppStateService, OptionsService, ViewContext, Permissions, IconDirective, C8yTranslateDirective, C8yTranslatePipe, SearchInputComponent, GetGroupIconPipe, LoadingComponent, EmptyStateComponent, CoreModule, CommonModule, CoreSearchModule, ModalModule, DeviceStatusModule, hookNavigator } from '@c8y/ngx-components'; import { debounce, get } from 'lodash-es'; import { BehaviorSubject, Subject, empty, of } from 'rxjs'; import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, Optional, Inject, EventEmitter, Output, Input, Component, forwardRef, ViewChild, ViewChildren, NgModule } from '@angular/core'; import { Router, ActivationEnd } from '@angular/router'; import * as i2 from '@c8y/client'; import { InventoryService, UserService, QueriesUtil } from '@c8y/client'; import { ApiService } from '@c8y/ngx-components/api'; import { filter, first, mergeMap, takeUntil, tap, startWith, switchMap, map } from 'rxjs/operators'; import * as i1$1 from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core'; import { CollapseDirective, CollapseModule } from 'ngx-bootstrap/collapse'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { isArray, isNumber, isString, isObject } from 'lodash'; import { NgIf, NgStyle, NgClass, NgFor, AsyncPipe } from '@angular/common'; import * as i4 from '@angular/forms'; import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'; var AssetsNavigatorAction; (function (AssetsNavigatorAction) { AssetsNavigatorAction[AssetsNavigatorAction["FETCH"] = 0] = "FETCH"; AssetsNavigatorAction[AssetsNavigatorAction["NEXT"] = 1] = "NEXT"; AssetsNavigatorAction[AssetsNavigatorAction["REFRESH"] = 2] = "REFRESH"; AssetsNavigatorAction[AssetsNavigatorAction["LOADING_DONE"] = 3] = "LOADING_DONE"; })(AssetsNavigatorAction || (AssetsNavigatorAction = {})); class LoadMoreNode extends NavigatorNode { static { this.NAME = 'LoadMoreNode'; } constructor() { super(); this.label = gettext('Load more'); this.icon = 'plus'; this.droppable = true; this.priority = -Infinity; } toString() { return LoadMoreNode.NAME; } isGroup() { return false; } } class AssetNode extends NavigatorNode { static { this.NAME = 'AssetNode'; } get hasChildren() { return this.root || this.service.isGroup(this.mo); } get isDevice() { return !!this.mo.c8y_IsDevice; } get isDeviceOrProbablyChildDevice() { return this.isDevice || this.isNeitherDeviceOrGroup; } get isNeitherDeviceOrGroup() { return (!this.service.isGroup(this.mo) && !this.service.isDynamicGroup(this.mo) && !this.isDevice && !this.root); } constructor(service, config = {}) { super(config); this.service = service; this.config = config; this.hideDevices = false; this.filterQuery$ = new BehaviorSubject(''); this.showChildDevices = false; /** * Asset node children (subentries). */ this.children = []; this.nodesFetched = new Subject(); this.root = this.root || false; this.hideDevices = config.hideDevices ?? this.hideDevices; this.mo = this.mo || {}; this.path = this.getPath(); this.draggable = !this.service?.moduleConfig?.disableDragAndDrop && !this.root; this.droppable = !this.service?.moduleConfig?.disableDragAndDrop && !this.isDeviceOrProbablyChildDevice; this.routerLinkExact = this.root; this.updateIcon(false); this.onUpdateSubscription = this.service .onUpdate(this) .subscribe(({ data, method }) => this.refresh(data, method)); this.setLabel(); this.iconComponent = this.isDeviceOrProbablyChildDevice ? DeviceStatusComponent : undefined; } getPath() { if (this.config.path) { return this.config.path; } return this.root ? 'group' : this.isDeviceOrProbablyChildDevice ? `device/${this.mo.id}` : `group/${this.mo.id}`; } refresh(mo = {}, method = 'GET') { if (mo?.id === this.mo.id) { this.mo = mo; this.setLabel(); } else if (method === 'DELETE') { this.parents.forEach((node) => node.refresh()); return; } if (this.events) { this.events.next(AssetsNavigatorAction.REFRESH); } } setLabel() { if (this.config.label || this.root) { this.label = this.config.label || gettext('Groups'); this.translateLabel = true; } else { this.label = this.service.label(this.mo); this.translateLabel = false; } } click(options = {}) { if (this.isDeviceOrProbablyChildDevice && !this.showChildDevices) { this.service.preferBreadcrumb(this.parents); return; } this.hookEvents(); this.updateIcon(options.open); if (options.open) { this.events.next(AssetsNavigatorAction.FETCH); } } sort() { this.children.sort((a, b) => { if (a.priority > b.priority) { return -1; } else if (a.priority < b.priority) { return 1; } else { return 0; } }); } addManagedObject(mo) { const { childAdditions } = this.mo; if (!this.isChildAddition(childAdditions, mo)) { this.add(this.service.createChildNode(mo, { hideDevices: this.hideDevices })); } } isChildAddition(childAdditions, mo) { return (childAdditions && childAdditions.references.some(({ managedObject: { id } }) => id === mo.id)); } destroy() { this.onUpdateSubscription.unsubscribe(); } get canDrop() { const nodeToMove = this.service.draggedData; if (nodeToMove) { const shouldGetChildOfItsOwn = !!nodeToMove.find(child => child === this); const isAlreadyChild = this.children.some(child => child.mo && child.mo.id === nodeToMove.mo.id); const preventMove = this === nodeToMove || shouldGetChildOfItsOwn || isAlreadyChild; return this.droppable && !preventMove && this.service.canDropNode(this.root); } return this.droppable; } dragStart($event) { super.dragStart($event); this.service.draggedData = this; this.service.rootNode.droppable = !this.isDeviceOrProbablyChildDevice; } dragEnd($event) { super.dragEnd($event); } async drop($event) { const nodeToMove = this.service.draggedData; // TODO remove when asset type node can be used on the root level. if (this.root && this.isAsset(nodeToMove)) { this.service.alert.info(gettext('Asset type node cannot become root node.')); this.draggedHover = false; this.service.draggedData = undefined; return; } super.drop($event); if (this.canDrop) { await this.moveNode(nodeToMove); } else { this.draggedHover = false; this.service.draggedData = undefined; } } hookEvents() { if (!this.events) { this.events = new Subject(); this.events.subscribe(evt => { if (!this.loading) { this.handleEvent(evt); } }); } } toString() { return AssetNode.NAME; } /** * Checks if the current node has child devices. */ hasChildDevices() { return this.mo && this.mo.c8y_IsDevice && this.mo.childDevices.references.length > 0; } fetch() { return this.root ? this.service.getRootNodes() : this.service.getGroupItems(this.mo.id, this.hideDevices ? { query: `$filter=(has(${GroupFragment.groupFragmentType}))$orderby=name` } : {}); } async updateIcon(open) { this.icon = await this.service.icon( // if it's root we are going to pass a fake mo to get the same icon as groups this.root ? { c8y_IsDeviceGroup: {} } : this.mo, open); } countChildren() { return this.children.length; } async handleEvent(evt) { if (!this.countChildren() && evt === AssetsNavigatorAction.FETCH) { this.loading = true; this.addNodes(await this.fetch()); this.loading = false; } else if (evt === AssetsNavigatorAction.NEXT) { this.loadMoreNode.loading = true; this.addNodes(await this.paging.next()); this.loadMoreNode.loading = false; } else if (evt === AssetsNavigatorAction.REFRESH) { this.loading = false; this.paging = undefined; this.loadMoreNode = undefined; this.empty(); this.events.next(AssetsNavigatorAction.FETCH); } } addNodes(res) { if (res.paging) { const { currentPage, nextPage, pageSize } = (this.paging = res.paging); if (currentPage === 1) { this.empty(); } const itemsCount = res.data.length; const moreItemsAvailable = !!nextPage && itemsCount === pageSize; this.toggleLoadMore(moreItemsAvailable); } (res.data || res).map(mo => { return this.addManagedObject(mo); }); this.events.next(AssetsNavigatorAction.LOADING_DONE); this.nodesFetched.next(); } toggleLoadMore(show) { if (!this.loadMoreNode && show) { this.loadMoreNode = new LoadMoreNode(); this.add(this.loadMoreNode); this.loadMoreNode.click = debounce(() => this.events.next(AssetsNavigatorAction.NEXT), 300, { leading: true, trailing: false }); } if (this.loadMoreNode) { this.loadMoreNode.hidden = !show; } } async moveNode(nodeToMove) { try { const isCopy = await this.showDropConfirm(nodeToMove); await this.verifyNodeAccess(nodeToMove); await this.addMovedNode(nodeToMove); if (!isCopy) { await this.removeMovedNode(nodeToMove); } this.expand(); } catch (ex) { if (ex) { this.service.alert.addServerFailure(ex); } } finally { this.draggedHover = false; this.service.draggedData = undefined; } } async showDropConfirm(nodeToMove) { this.confirm.title = gettext('Move'); this.confirm.message = gettext('Do you want to move the group?'); const buttons = [ { label: gettext('Cancel'), action: () => Promise.reject() }, { label: gettext('Move'), status: 'default', action: () => Promise.resolve(false) } ]; if (nodeToMove.isDeviceOrProbablyChildDevice) { this.confirm.title = gettext('Move or add'); this.confirm.message = gettext('Do you want to move or add the device?'); buttons.push({ label: gettext('Add'), status: 'primary', action: () => Promise.resolve(true) }); } return this.confirm.show(buttons); } async verifyNodeAccess(nodeToMove) { return this.service.inventory.update({ id: nodeToMove.mo.id }); } async addMovedNode(nodeToMove) { let mo; if (this.root && !this.isAsset(nodeToMove)) { mo = (await this.service.inventory.update({ id: nodeToMove.mo.id, type: GroupFragment.groupType })).data; this.addManagedObject(mo); return; } mo = (await this.service.inventory.childAssetsAdd(nodeToMove.mo, this.mo)).data; this.addManagedObject(mo); } isAsset(nodeToMove) { // TODO use isAsset check when https://github.com/Cumulocity-IoT/cumulocity-ui/pull/690 is merged. // Do not override asset type! return nodeToMove.mo?.c8y_IsAsset; } async removeMovedNode(nodeToMove) { for (const parent of nodeToMove.parents) { if (parent.mo && parent.mo.type === GroupFragment.dynamicGroupType) { break; // smart groups don't need to be changed } if (parent.root && !this.isAsset(nodeToMove)) { await this.service.inventory.update({ id: nodeToMove.mo.id, type: GroupFragment.subGroupType }); } if (!parent.root) { await this.service.inventory.childAssetsRemove(nodeToMove.mo, parent.mo); } parent.remove(nodeToMove); } } } class DynamicGroupNode extends AssetNode { constructor(service, config = {}) { super(service, config); this.service = service; this.draggable = false; this.droppable = false; } get hasChildren() { return true; } get query() { return this.mo.c8y_DeviceQueryString; } fetch() { return this.service.getDynamicGroupItems(this.query); } } const ASSET_NAVIGATOR_CONFIG = new InjectionToken('AssetNodeConfig'); /** * @deprecated * Original service was moved to core/common. * This service is deprecated and serves as a proxy to maintain backward compatibility. */ class DeviceGroupService { constructor(migratedDeviceGroupService) { this.migratedDeviceGroupService = migratedDeviceGroupService; } icon(mo, open = false) { return this.migratedDeviceGroupService.getIcon(mo, open); } isGroup(mo) { return this.migratedDeviceGroupService.isGroup(mo); } isDynamicGroup(mo) { return this.migratedDeviceGroupService.isDynamicGroup(mo); } isDataBroker(mo) { return this.migratedDeviceGroupService.isDataBroker(mo); } isDataBrokerActive(mo) { return this.migratedDeviceGroupService.isDataBrokerActive(mo); } isAsset(mo) { return this.migratedDeviceGroupService.isAsset(mo); } isAnyGroup(mo) { return this.migratedDeviceGroupService.isAnyGroup(mo); } isDevice(mo) { return this.migratedDeviceGroupService.isDevice(mo); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceGroupService, deps: [{ token: i1.GroupService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceGroupService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceGroupService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.GroupService }] }); class AssetNodeService { constructor() { this.firstUrl = true; this.PAGE_SIZE = 20; this.inventory = inject(InventoryService); this.apiService = inject(ApiService); this.modal = inject(ModalService); this.alert = inject(AlertService); this.translateService = inject(TranslateService); this.breadcrumbService = inject(BreadcrumbService); this.user = inject(UserService); this.appState = inject(AppStateService); this.optionsService = inject(OptionsService); this.deviceGroupService = inject(DeviceGroupService); this.router = inject(Router); this.moduleConfig = { rootNodePriority: 2000, ...(inject(ASSET_NAVIGATOR_CONFIG, { optional: true }) || {}) }; this.queriesUtil = new QueriesUtil(); this.router.events .pipe(filter((event) => event instanceof ActivationEnd && event.snapshot.data?.contextData), first()) .subscribe(({ snapshot }) => { this.expandNodesOnStart(snapshot); }); } /** * Expands the navigator nodes on first navigation. * @param snapshot The current navigation snapshot. */ async expandNodesOnStart(snapshot) { if (!this.rootNode) { return; } if (snapshot.data?.context === ViewContext.Group || snapshot.data?.context === ViewContext.Device) { const { data } = await this.inventory.detail(snapshot.data.contextData, { withParents: true }); const allManagedObjectParentIds = [ ...data.assetParents.references.map(({ managedObject }) => { return managedObject.id; }), data.id ]; this.expandAll(this.rootNode, allManagedObjectParentIds); return; } if (this.moduleConfig.openOnStart) { this.expandAll(this.rootNode, []); } } /** * Expands all the given ids recursively. Stops if it does not find any. * @param node The node where the expanding should be started * @param ids The ids that should be expanded. */ expandAll(node, ids) { node.open = true; node.click({ open: true }); if (node.events) { node.events .pipe(filter(action => action === AssetsNavigatorAction.LOADING_DONE), first()) .subscribe(() => { const nodeToExpand = node.children.find(childNode => childNode.mo?.id && ids.includes(childNode.mo.id)); if (nodeToExpand) { this.expandAll(nodeToExpand, ids.filter(id => id !== nodeToExpand.mo.id)); } }); } } label(mo) { return (mo.name || (this.isDevice(mo) && this.translateService.instant(gettext('Device {{id}}'), { id: mo.id })) || '--'); } icon(mo, open) { return this.deviceGroupService.icon(mo, open); } isGroup(mo) { return this.deviceGroupService.isGroup(mo); } isDynamicGroup(mo) { return this.deviceGroupService.isDynamicGroup(mo); } isDataBroker(mo) { return this.deviceGroupService.isDataBroker(mo); } isDataBrokerActive(mo) { return this.deviceGroupService.isDataBrokerActive(mo); } isAsset(mo) { return this.deviceGroupService.isAsset(mo); } isAnyGroup(mo) { return this.deviceGroupService.isAnyGroup(mo); } isDevice(mo) { return this.deviceGroupService.isDevice(mo); } createRootNode(config = {}) { this.rootNode = this.createAssetNode({ root: true, ...config, priority: this.moduleConfig.rootNodePriority, featureId: 'groups' }); return this.rootNode; } createDynamicGroupNode(config) { return new DynamicGroupNode(this, config); } createAssetNode(config) { return new AssetNode(this, config); } createChildNode(managedObject, config) { const { type } = managedObject; config.mo = managedObject; if (type === GroupFragment.dynamicGroupType) { return this.createDynamicGroupNode(config); } return this.createAssetNode(config); } getRootNodes(customFilter) { const defaultFilter = { pageSize: this.PAGE_SIZE, withChildren: false, onlyRoots: !this.optionsService.disableOnlyRootsQuery, query: this.queriesUtil.buildQuery(this.navRootQueryFilter()) }; const groupFilter = { ...defaultFilter, ...customFilter }; // due to BE performance limitations we do not allow filtering and sorting for a user without inventory roles if (this.appState?.currentUser?.value && !this.user.hasAnyRole(this.appState.currentUser.value, [ Permissions.ROLE_INVENTORY_READ, Permissions.ROLE_MANAGED_OBJECT_READ ])) { delete groupFilter.query; Object.assign(groupFilter, { fragmentType: GroupFragment.groupFragmentType, onlyRoots: true }); } return this.inventory.list(this.createFilter(groupFilter)); } getAllInventories(customFilter) { const defaultFilter = { pageSize: this.PAGE_SIZE, withChildren: false }; const groupFilter = { ...defaultFilter, ...customFilter }; return this.inventory.list(this.createFilter(groupFilter)); } getGroupItems(moId, extraFilter = {}, withChildren = false, filterQuery = '') { const queryFilter = { withChildren, pageSize: this.PAGE_SIZE, query: this.groupQueryFilter(moId, filterQuery) }; return this.inventory.childAssetsList(moId, { ...queryFilter, ...extraFilter }); } getUnassignedDevices(withChildren = false, filterQuery = '') { const queryFilter = { fragmentType: 'c8y_IsDevice', onlyRoots: true, withChildren, pageSize: this.PAGE_SIZE, q: this.getUnassignedDevicesQueryStr(filterQuery) }; return this.inventory.list(this.createFilter(queryFilter)); } getDynamicGroupItems(groupQuery, filterObj = {}) { const { query, ...queryParams } = filterObj; const orderByQuery = query; const queryFilter = { q: this.buildCombinedQuery(groupQuery, orderByQuery), ...queryParams }; return this.inventory.list(this.createFilter(queryFilter)); } getDeviceChildren(moId, extraFilter = {}, filterQuery = '', withChildren = false) { const queryFilter = { withChildren, pageSize: this.PAGE_SIZE, query: this.groupQueryFilter(moId, filterQuery) }; return this.inventory.childDevicesList(moId, { ...queryFilter, ...extraFilter }); } getUnassignedDevicesQueryStr(filterQuery) { const hasGroupId = filterQuery.includes('bygroupid'); // Fetch all unassigned devices. const defaultQueryStr = '$orderby=name'; // filterQuery is a custom query to fetch unassigned devices filtered by name. return hasGroupId || !filterQuery ? defaultQueryStr : filterQuery; } groupQueryFilter(moId, filterQuery) { if (!filterQuery) { return `$filter=(bygroupid(${moId}))$orderby=name`; } return filterQuery; } navRootQueryFilter() { const navRootFilter = this.rootQueryFilter(); navRootFilter.__orderby = [{ name: 1 }]; return navRootFilter; } rootQueryFilter() { const { moduleConfig } = this; const rootFilter = this.optionsService.disableOnlyRootsQuery ? { __filter: { type: GroupFragment.groupType }, __orderby: [] } : { __filter: { __has: GroupFragment.groupFragmentType }, __orderby: [] }; if (moduleConfig.smartGroups) { const queryFilter = { __filter: { __and: [ { type: GroupFragment.dynamicGroupType }, { __has: GroupFragment.dynamicGroupFragment }, { __not: { __has: `${GroupFragment.dynamicGroupFragment}.invisible` } } ] } }; this.queriesUtil.addOrFilter(rootFilter, queryFilter); } return rootFilter; } onUpdate({ mo, root }) { if (mo.id) { return this.apiService .hookResponse(({ url, method }) => ['PUT', 'DELETE', 'POST'].includes(method) && RegExp(`inventory/managedObjects/${mo.id}`).test(url)) .pipe(filter(() => !this.draggedData), this.apiService.excludePermissionCall(), mergeMap((this.apiService.resolveData)), filter(response => !response?.data?.c8y_Dashboard)); } else if (root) { return this.apiService .hookResponse(({ url, method }) => RegExp('inventory/managedObjects/?$').test(url) && method === 'POST') .pipe(mergeMap((this.apiService.resolveData)), filter(response => this.isNewManagedObjectRoot(response))); } else { return empty(); } } isNewManagedObjectRoot(response = {}) { const { data } = response; let isRootAsset = false; if (typeof data === 'object') { isRootAsset = !!data[GroupFragment.groupFragmentType]; if (!isRootAsset && this.moduleConfig.smartGroups) { isRootAsset = !!data[GroupFragment.dynamicGroupFragment]; } } return isRootAsset; } /** * Check if it is possible to drop a node after dragging. * @param dropOnRoot Is the drop performed on the root node */ canDropNode(dropOnRoot) { return (!dropOnRoot || this.user.hasAnyRole(this.appState.currentUser.value, [ Permissions.ROLE_INVENTORY_ADMIN, Permissions.ROLE_MANAGED_OBJECT_ADMIN ])); } /** * There could be multiple breadcrumbs for devices, * so we set a preferred one on click on a device. * @param parents The parent nodes of the device to select the prefered one. */ preferBreadcrumb(parents) { if (parents.length === 1) { this.breadcrumbService.selectPreferredByPath(parents[0].path); } } createFilter(extraParams = {}) { const params = { currentPage: 1, withTotalPages: true, pageSize: 10 }; return { ...params, ...extraParams }; } buildCombinedQuery(queryA, queryB) { let combinedQuery; if (queryA && queryB) { const filterQuery = this.queriesUtil.buildQuery([ { __useFilterQueryString: queryA }, { __useFilterQueryString: queryB } ]); const orberByQuery = this.queriesUtil.extractAndMergeOrderBys([queryA, queryB]); combinedQuery = `${filterQuery} ${orberByQuery}`; } else { combinedQuery = queryA || queryB || ''; } return combinedQuery; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetNodeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetNodeService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetNodeService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class AssetNodeFactory { constructor(service, moduleConfig) { this.service = service; this.moduleConfig = moduleConfig; } get() { const rootNavigatorNode = get(this.moduleConfig, 'rootNavigatorNode') ?? true; let { rootNode } = this.service; if (rootNavigatorNode === false) { return; } if (!rootNode) { rootNode = this.service.createRootNode(rootNavigatorNode === true ? {} : rootNavigatorNode); } return rootNode; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetNodeFactory, deps: [{ token: AssetNodeService }, { token: ASSET_NAVIGATOR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetNodeFactory }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetNodeFactory, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: AssetNodeService }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [ASSET_NAVIGATOR_CONFIG] }] }] }); class GroupNode extends AssetNode { static { this.NAME = 'GroupNode'; } /** * Creates a new node which shows only groups. * * @param service The service to use. * @param config The default configuration of the node. * @param groupsOnly Set this true, if only groups should be shown. * @param selectable Set this true, if it is selectable. */ constructor(service, config = {}) { super(service, config); this.service = service; /** * Set this true, if only groups should be shown. */ this.groupsOnly = false; /** * Set this true, if it groups are also selectable. */ this.groupsSelectable = false; /** * Devices with children can be selected to show their child devices. */ this.showChildDevices = false; /** * Group node children (subentries). */ this.children = []; this.groupsOnly = config.groupsOnly || false; this.groupsSelectable = config.groupsSelectable || false; this.showChildDevices = config.showChildDevices || false; } /** * Adds the MO as a child node. * @param mo ManagedObject */ addManagedObject(mo) { const { childAdditions } = this.mo; if (!this.isChildAddition(childAdditions, mo)) { this.add(this.service.createChildNode({ mo, groupsOnly: this.groupsOnly, groupsSelectable: this.groupsSelectable, showChildDevices: this.showChildDevices })); } } /** * Counts the number of children for the current node (with the exception of the UnassignedDevicesNode). */ countChildren() { return this.children.filter(value => value.toString() !== 'UnassignedDevicesNode').length; } /** * Removes all child nodes except the UnassignedDevicesNode. */ empty() { this.children = this.children.filter(value => value.toString() === 'UnassignedDevicesNode'); } fetch() { const isRoot = this.root; const isDevice = this.mo.c8y_IsDevice; return isRoot ? this.service.getRootNodes() : isDevice ? this.service.getDeviceChildren(this.mo.id, {}, this.filterQuery$.value, this.showChildDevices) : this.service.getGroupItems(this.mo.id, this.groupsOnly ? { query: `$filter=(has(${GroupFragment.groupFragmentType}))` } : {}, this.showChildDevices, this.filterQuery$.value); } toString() { return GroupNode.NAME; } isGroup() { return this.mo && this.service.isGroup(this.mo); } } class GroupNodeService extends AssetNodeService { constructor() { super(...arguments); this.PAGE_SIZE = 5; } createGroupNode(config) { return new GroupNode(this, config); } createChildNode(config) { return this.createGroupNode(config); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: GroupNodeService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: GroupNodeService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: GroupNodeService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class AssetSelectorService extends AssetNodeService { constructor() { super(...arguments); /** * Function which will check if the node is selectable. */ this.isNodeSelectableFn = true; } /** * Sets the function that will decide if the node is selectable. * @param fn A boolean or a function that will decide if the node is selectable. */ setIsNodeSelectable(fn) { this.isNodeSelectableFn = fn; } /** * Checks if the node is selectable. * @param node The node to check. */ isNodeSelectable(node) { if (typeof this.isNodeSelectableFn === 'boolean') { return this.isNodeSelectableFn; } else if (typeof this.isNodeSelectableFn === 'function') { return this.isNodeSelectableFn(node); } return true; } /** * Simplifies the object model based on the selected mode. * @param obj The selected asset. * @param mode The mode which will decide what type of model will be returned. */ normalizeValue(obj, modelMode) { return this.simplifyModel(this.normalizeModelValue(obj), modelMode); } simplifyModel(model, mode) { const mapModel = model => { const { id, name, c8y_DeviceQueryString } = model; return { id, name, ...(c8y_DeviceQueryString ? { c8y_DeviceQueryString } : {}) }; }; if (mode === 'full') { return model; } if (!isArray(model)) { return mapModel(model); } return model.map(mapModel); } /** * Returns the index of the currently selected item. * @param selected All selected items * @param selectedMo The new selected item- * @returns An index, or -1 if not found. */ getIndexOfSelected(selected, selectedMo) { return selected.findIndex(mo => mo.id === selectedMo.id); } normalizeModelValue(value) { if (isNumber(value) || isString(value)) { return [{ id: value }]; } if (isArray(value)) { return value; } if (isObject(value)) { return [value]; } return []; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetSelectorService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetSelectorService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetSelectorService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class AssetSelectorNodeComponent { /** * @ignore */ get expandTitle() { return !this.node.open ? gettext('Expand') : gettext('Collapse'); } /** * @ignore only di */ constructor(translateService, cd, assetSelectorService) { this.translateService = translateService; this.cd = cd; this.assetSelectorService = assetSelectorService; /** * All preselected items. */ this.preselected = []; /** * Should the path be shown. */ this.showPath = false; /** * Can the user select multiple assets. */ this.multi = false; /** * The current path to the node. */ this.view = 'tree'; this.disabled = false; /** * Event, which indicates whether the loading of the node has completed. */ this.isLoadingState = new EventEmitter(); /** * Event that emits when a node is selected. */ this.onSelect = new EventEmitter(); /** * Event that emits when a node is deselected. */ this.onDeselect = new EventEmitter(); /** * @ignore */ this.level = 0; /** * @ignore */ this.unsubscribe$ = new Subject(); this.isNodeSelectable = true; /** sets the `btn-pending` class in the load more button */ this.isLoading = false; } /** * @ignore */ async ngOnInit() { this.isNodeSelectable = this.assetSelectorService.isNodeSelectable(this.node); this.breadcrumb = this.node.label; this.setupBreadcrumbsAndLevel(this.node); if (this.node instanceof GroupNode) { this.node.hookEvents(); } // open on startup if (this.node.root) { this.click(); } // used for loading and to trigger change detection when the node is no longer loading. if (this.node.events) { this.node.events .pipe(takeUntil(this.unsubscribe$), filter((a) => a === AssetsNavigatorAction.LOADING_DONE)) .subscribe(() => { this.isLoadingState.emit(false); this.cd.markForCheck(); }); } } /** * Opens a node. */ click() { this.node.open = !this.node.open; this.node.click({ open: this.node.open }); } setupBreadcrumbsAndLevel(node) { if (node.parents && node.parents.length) { const parent = node.parents[0]; this.breadcrumb = this.translateService.instant(parent.label) + ' > ' + this.translateService.instant(this.breadcrumb); this.level++; this.setupBreadcrumbsAndLevel(parent); } } /** * Selects the node and emits a change on the parent component. * @param node The node to select. */ selected(node) { if (node.mo) { this.updateSelection(node.mo); return; } this.click(); } /** * Handles clicks on a item in Miller View. * @param node The node that was clicked. */ millerViewClick(node) { node.breadcrumb = this.breadcrumb; if (!this.handleNextMillerViewColumn) { return; } const shouldHandleDefault = this.handleNextMillerViewColumn(node, this.index); if (shouldHandleDefault) { this.selected(node); } } /** * @ignore */ ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } isSelected() { if (!this.node.mo) { return false; } return this.assetSelectorService.getIndexOfSelected(this.preselected, this.node.mo) > -1; } isActive() { if (this.active && this.node.mo) { return this.active.mo?.id === this.node.mo.id; } return false; } updateSelection(selectedMo) { if (!this.multi) { this.onDeselect.emit({ deselectMode: 'all', mo: selectedMo }); return; } if (this.isSelected()) { this.onDeselect.emit({ deselectMode: 'single', mo: selectedMo }); return; } this.onSelect.emit(selectedMo); this.cd.markForCheck(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AssetSelectorNodeComponent, deps: [{ token: i1$1.TranslateService }, { token: i0.ChangeDetectorRef }, { token: AssetSelectorService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: AssetSelectorNodeComponent, isStandalone: true, selector: "c8y-asset-selector-node", inputs: { node: "node", rootNode: "rootNode", preselected: "preselected", showPath: "showPath", multi: "multi", view: "view", index: "index", active: "active", handleNextMillerViewColumn: "handleNextMillerViewColumn", disabled: "disabled" }, outputs: { isLoadingState: "isLoadingState", onSelect: "onSelect", onDeselect: "onDeselect" }, ngImport: i0, template: "<!-- Hierarchy tree -->\n<div\n class=\"c8y-asset-selector__item\"\n [ngStyle]=\"{\n 'margin-left': level > 1 ? 16 + 'px' : '0'\n }\"\n *ngIf=\"view === 'tree'\"\n [attr.role]=\"view === 'tree' ? 'tree' : 'list'\"\n [ngClass]=\"{\n 'c8y-asset-selector__item--more': node?.icon === 'plus',\n 'c8y-asset-selector__item--start': level === 0\n }\"\n>\n <div\n class=\"c8y-asset-selector__node\"\n title=\"{{ breadcrumb | translate }}\"\n *ngIf=\"node && !node.root && !node.hidden\"\n [attr.role]=\"view === 'tree' ? 'treeitem' : 'listitem'\"\n [ngClass]=\"{ 'c8y-asset-selector__node--open': node?.open }\"\n >\n <!-- expand button -->\n <div class=\"c8y-asset-selector__node__btn-spacer\">\n <button\n class=\"collapse-btn btn-dot\"\n [title]=\"expandTitle | translate\"\n [attr.aria-expanded]=\"node.open\"\n (click)=\"click()\"\n *ngIf=\"node.isGroup() || node.hasChildDevices()\"\n >\n <i c8yIcon=\"angle-right\"></i>\n </button>\n </div>\n <div\n class=\"d-flex a-i-center p-t-4 p-b-4\"\n *ngIf=\"node.toString() !== 'LoadMoreNode' && isNodeSelectable\"\n >\n <label [ngClass]=\"{ 'c8y-checkbox': multi, 'c8y-radio': !multi }\">\n <input\n id=\"nodeLabel\"\n [type]=\"multi ? 'checkbox' : 'radio'\"\n (change)=\"selected(node)\"\n [checked]=\"isSelected()\"\n [disabled]=\"disabled || !node.groupsSelectable && node.isGroup()\"\n />\n <span></span>\n <span\n class=\"sr-only\"\n for=\"nodeLabel\"\n translate\n >\n Node label\n </span>\n </label>\n </div>\n\n <!-- group button -->\n <button\n class=\"c8y-asset-selector__btn text-truncate\"\n [attr.aria-expanded]=\"!node.open\"\n *ngIf=\"node.isGroup() || node.hasChildDevices()\"\n (click)=\"click()\"\n >\n <i\n class=\"c8y-icon c8y-icon-duocolor m-r-4 text-16\"\n [c8yIcon]=\"node.icon\"\n [title]=\"'Smart group' | translate\"\n *ngIf=\"node.icon === 'c8y-group-smart'\"\n ></i>\n <i\n class=\"c8y-icon c8y-icon-duocolor m-r-4 text-16\"\n [c8yIcon]=\"node.icon\"\n [title]=\"'Group' | translate\"\n *ngIf=\"node.icon !== 'c8y-group-smart'\"\n ></i>\n <span title=\"{{ breadcrumb }}\">\n {{ node.translateLabel ? (node.label | translate) : node.label }}\n <!-- use just for search results to display the path -->\n <p\n class=\"text-truncate\"\n *ngIf=\"showPath\"\n >\n <small\n class=\"text-muted\"\n title=\"{{ breadcrumb }}\"\n >\n <em>{{ breadcrumb }}</em>\n </small>\n </p>\n <!-- up to here -->\n </span>\n </button>\n <!-- not a group button -->\n <button\n class=\"flex-grow\"\n title=\"{{ breadcrumb }}\"\n type=\"button\"\n *ngIf=\"!node.isGroup() && !node.hasChildDevices()\"\n [ngClass]=\"{\n 'btn btn-default btn-sm m-b-8 m-r-8 d-flex j-c-center': node.icon === 'plus',\n 'c8y-asset-selector__btn text-truncate': node.icon != 'plus'\n }\"\n (click)=\"selected(node)\"\n >\n <i\n class=\"c8y-icon c8y-icon-duocolor m-r-4 text-16\"\n [c8yIcon]=\"node.icon\"\n [title]=\"'Smart group' | translate\"\n *ngIf=\"node.icon === 'c8y-group-smart'\"\n ></i>\n <i\n class=\"c8y-icon m-r-4\"\n [c8yIcon]=\"node.icon\"\n [title]=\"'Group' | translate\"\n *ngIf=\"node.icon !== 'c8y-group-smart'\"\n [ngClass]=\"{ 'c8y-icon-duocolor text-16 ': node.icon != 'plus' }\"\n ></i>\n <span title=\"{{ breadcrumb }}\">\n {{ node.translateLabel ? (node.label | translate) : node.label }}\n <!-- use just for search results to display the path -->\n <p\n class=\"text-truncate text-muted small\"\n *ngIf=\"showPath\"\n >\n <em>{{ breadcrumb }}</em>\n </p>\n <!-- up to here -->\n </span>\n </button>\n </div>\n <div\n class=\"collapse\"\n *ngIf=\"node.countChildren()\"\n [collapse]=\"!node.open\"\n [isAnimated]=\"true\"\n [attr.role]=\"'group'\"\n >\n <c8y-asset-selector-node\n *ngFor=\"let childNode of node.children\"\n [node]=\"childNode\"\n [preselected]=\"preselected || []\"\n [disabled]=\"disabled\"\n [multi]=\"multi\"\n [active]=\"active\"\n [attr.role]=\"view === 'tree' ? 'treeitem' : 'listitem'\"\n (onSelect)=\"onSelect.emit($event)\"\n (onDeselect)=\"onDeselect.emit($event)\"\n ></c8y-asset-selector-node>\n </div>\n</div>\n\n<!-- Miller columns -->\n<div *ngIf=\"view === 'miller'\">\n <div\n class=\"miller-column__item bg-inherit\"\n title=\"{{ breadcrumb | translate }}\"\n *ngIf=\"node && !node.root && !node.hidden && node !== rootNode\"\n [ngClass]=\"{\n active: isActive(),\n 'miller-column__item--more': node.toString() === 'LoadMoreNode'\n }\"\n >\n <div\n class=\"m-l-4 m-r-4 miller-column__item__checkbox\"\n *ngIf=\"node.toString() !== 'LoadMoreNode'\"\n >\n <label [ngClass]=\"{ 'c8y-radio': !multi, 'c8y-checkbox': multi }\">\n <input\n id=\"nodeLabel2\"\n [type]=\"multi ? 'checkbox' : 'radio'\"\n (change)=\"selected(node)\"\n [checked]=\"isSelected()\"\n [disabled]=\"disabled || !node.groupsSelectable && node.isGroup()\"\n />\n <span></span>\n <span\n class=\"sr-only\"\n for=\"nodeLabel2\"\n translate\n >\n Node label\n </span>\n </label>\n </div>\n\n <button\n title=\"{{ breadcrumb | translate }}\"\n type=\"button\"\n [ngClass]=\"{\n 'btn btn-default btn-sm d-flex flex-grow j-c-center m-l-16 m-r-16 m-b-4 m-t-4':\n node.toString() === 'LoadMoreNode',\n 'miller-column__item__btn': node.toString() !== 'LoadMoreNode',\n 'btn-pending': node.loading && node.toString() === 'LoadMoreNode'\n }\"\n (click)=\"millerViewClick(node)\"\n >\n <i\n class=\"c8y-icon m-r-4\"\n [c8yIcon]=\"node.icon\"\n [ngClass]=\"{ 'c8y-icon-duocolor text-16': node.toString() !== 'LoadMoreNode' }\"\n ></i>\n <div class=\"text-left text-truncate\">\n <p\n class=\"text-truncate\"\n title=\"{{ node.translateLabel ? (node.label | translate) : node.label }}\"\n >\n {{ node.translateLabel ? (node.label | translate) : node.label }}\n </p>\n <!-- use just for search results to display the path -->\n <small\n class=\"text-muted text-truncate\"\n title=\"{{ breadcrumb }}\"\n *ngIf=\"showPath\"\n >\n <em>{{ breadcrumb }}</em>\n </small>\n <!-- up to here -->\n </div>\n <span\n class=\"p-l-4 m-l-auto\"\n *ngIf=\"node.isGroup() || node.hasChildDevices()\"\n >\n <i c8yIcon=\"angle-right\"></i>\n </span>\n </button>\n </div>\n\n <div\n role=\"list\"\n *ngIf=\"node\"\n [ngClass]=\"{ hidden: node !== rootNode }\"\n >\n <c8y-asset-selector-node\n role=\"listitem\"\n *ngFor=\"let childNode of node.children\"\n [node]=\"childNode\"\n [rootNode]=\"rootNode\"\n [preselected]=\"preselected || []\"\n [multi]=\"multi\"\n [view]=\"view\"\n [index]=\"index\"\n [active]=\"active\"\n [disabled]=\"disabled\"\n [handleNextMillerViewColumn]=\"handleNextMillerViewColumn\"\n (onSelect)=\"onSelect.emit($event)\"\n (onDeselect)=\"onDeselect.emit($event)\"\n ></c8y-asset-selector-node>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: AssetSelectorNodeComponent, selector: "c8y-asset-selector-node", inputs: ["node", "rootNode", "preselected", "showPath", "multi", "view", "index", "active", "handleNextMillerViewColumn", "disabled"], outputs: ["isLoadingState", "onSelect", "onDeselect"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }