UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 152 kB
{"version":3,"file":"c8y-ngx-components-assets-navigator.mjs","sources":["../../assets-navigator/action.enum.ts","../../assets-navigator/load-more-node.ts","../../assets-navigator/asset-node.ts","../../assets-navigator/dynamic-group-node.ts","../../assets-navigator/asset-node-config.model.ts","../../assets-navigator/group.service.ts","../../assets-navigator/asset-node.service.ts","../../assets-navigator/asset-node.factory.ts","../../assets-navigator/asset-selector/group-node.ts","../../assets-navigator/asset-selector/group-node.service.ts","../../assets-navigator/asset-selector/asset-selector.service.ts","../../assets-navigator/asset-selector/asset-selector-node.component.ts","../../assets-navigator/asset-selector/asset-selector-node.component.html","../../assets-navigator/asset-selector/asset-selector-base.ts","../../assets-navigator/asset-selector/asset-selector.model.ts","../../assets-navigator/asset-selector/unassigned-devices-node.ts","../../assets-navigator/asset-selector/asset-selector.component.ts","../../assets-navigator/asset-selector/asset-selector.component.html","../../assets-navigator/asset-selector/miller-view.component.ts","../../assets-navigator/asset-selector/miller-view.component.html","../../assets-navigator/asset-selector/asset-selector.module.ts","../../assets-navigator/assets-navigator.module.ts","../../assets-navigator/c8y-ngx-components-assets-navigator.ts"],"sourcesContent":["export enum AssetsNavigatorAction {\n FETCH,\n NEXT,\n REFRESH,\n LOADING_DONE\n}\n","import { gettext } from '@c8y/ngx-components/gettext';\nimport { NavigatorNode } from '@c8y/ngx-components';\n\nexport class LoadMoreNode extends NavigatorNode {\n static NAME = 'LoadMoreNode';\n label = gettext('Load more');\n icon = 'plus';\n droppable = true;\n\n constructor() {\n super();\n this.priority = -Infinity;\n }\n\n toString() {\n return LoadMoreNode.NAME;\n }\n\n isGroup() {\n return false;\n }\n}\n","import { IIdentified, Paging } from '@c8y/client';\nimport {\n ClickOptions,\n DeviceStatusComponent,\n GroupFragment,\n NavigatorNode,\n NavigatorNodeData\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { debounce } from 'lodash-es';\nimport { BehaviorSubject, Subject, Subscription } from 'rxjs';\nimport { AssetsNavigatorAction } from './action.enum';\nimport { AssetNodeMo, AssetNodeService } from './asset-node.service';\nimport { LoadMoreNode } from './load-more-node';\n\nexport class AssetNode extends NavigatorNode {\n static NAME = 'AssetNode';\n root: boolean;\n mo: any;\n hideDevices = false;\n filterQuery$ = new BehaviorSubject<string>('');\n showChildDevices = false;\n\n /**\n * Asset node children (subentries).\n */\n children: AssetNode[] = [];\n\n get hasChildren() {\n return this.root || this.service.isGroup(this.mo);\n }\n\n get isDevice() {\n return !!this.mo.c8y_IsDevice;\n }\n\n get isDeviceOrProbablyChildDevice() {\n return this.isDevice || this.isNeitherDeviceOrGroup;\n }\n\n get isNeitherDeviceOrGroup() {\n return (\n !this.service.isGroup(this.mo) &&\n !this.service.isDynamicGroup(this.mo) &&\n !this.isDevice &&\n !this.root\n );\n }\n\n events: Subject<AssetsNavigatorAction>;\n nodesFetched: Subject<void> = new Subject();\n protected paging: Paging<AssetNodeMo>;\n protected loadMoreNode: LoadMoreNode;\n private onUpdateSubscription: Subscription;\n\n constructor(\n protected service: AssetNodeService,\n protected config: NavigatorNodeData = {}\n ) {\n super(config);\n\n this.root = this.root || false;\n this.hideDevices = config.hideDevices ?? this.hideDevices;\n this.mo = this.mo || {};\n this.path = this.getPath();\n this.draggable = !this.service?.moduleConfig?.disableDragAndDrop && !this.root;\n this.droppable =\n !this.service?.moduleConfig?.disableDragAndDrop && !this.isDeviceOrProbablyChildDevice;\n this.routerLinkExact = this.root;\n this.updateIcon(false);\n this.onUpdateSubscription = this.service\n .onUpdate(this)\n .subscribe(({ data, method }) => this.refresh(data, method));\n this.setLabel();\n this.iconComponent = this.isDeviceOrProbablyChildDevice ? DeviceStatusComponent : undefined;\n }\n\n getPath() {\n if (this.config.path) {\n return this.config.path;\n }\n\n return this.root\n ? 'group'\n : this.isDeviceOrProbablyChildDevice\n ? `device/${this.mo.id}`\n : `group/${this.mo.id}`;\n }\n\n refresh(mo: any = {}, method = 'GET') {\n if (mo?.id === this.mo.id) {\n this.mo = mo;\n this.setLabel();\n if (this.refreshCallback) {\n this.refreshCallback();\n }\n } else if (method === 'DELETE') {\n this.parents.forEach((node: AssetNode) => node.refresh());\n return;\n }\n if (this.events) {\n this.events.next(AssetsNavigatorAction.REFRESH);\n }\n }\n\n setLabel() {\n if (this.config.label || this.root) {\n this.label = this.config.label || gettext('Groups');\n this.translateLabel = true;\n } else {\n this.label = this.service.label(this.mo);\n this.translateLabel = false;\n }\n }\n\n click(options: ClickOptions = {}) {\n if (this.isDeviceOrProbablyChildDevice && !this.showChildDevices) {\n this.service.preferBreadcrumb(this.parents);\n return;\n }\n this.hookEvents();\n this.updateIcon(options.open);\n if (options.open) {\n this.events.next(AssetsNavigatorAction.FETCH);\n }\n }\n\n sort() {\n this.children.sort((a, b) => {\n if (a.priority > b.priority) {\n return -1;\n } else if (a.priority < b.priority) {\n return 1;\n } else {\n return 0;\n }\n });\n }\n\n addManagedObject(mo) {\n const { childAdditions } = this.mo;\n if (!this.isChildAddition(childAdditions, mo)) {\n this.add(this.service.createChildNode(mo, { hideDevices: this.hideDevices }));\n }\n }\n\n isChildAddition(childAdditions, mo) {\n return (\n childAdditions && childAdditions.references.some(({ managedObject: { id } }) => id === mo.id)\n );\n }\n\n destroy() {\n this.onUpdateSubscription.unsubscribe();\n }\n\n get canDrop() {\n const nodeToMove = this.service.draggedData;\n if (nodeToMove) {\n const shouldGetChildOfItsOwn = !!nodeToMove.find(child => child === this);\n const isAlreadyChild = (this.children as AssetNode[]).some(\n child => child.mo && child.mo.id === nodeToMove.mo.id\n );\n const preventMove = this === nodeToMove || shouldGetChildOfItsOwn || isAlreadyChild;\n return this.droppable && !preventMove && this.service.canDropNode(this.root);\n }\n return this.droppable;\n }\n\n dragStart($event) {\n super.dragStart($event);\n this.service.draggedData = this;\n this.service.rootNode.droppable = !this.isDeviceOrProbablyChildDevice;\n }\n\n dragEnd($event) {\n super.dragEnd($event);\n }\n\n async drop($event) {\n const nodeToMove = this.service.draggedData;\n\n // TODO remove when asset type node can be used on the root level.\n if (this.root && this.isAsset(nodeToMove)) {\n this.service.alert.info(gettext('Asset type node cannot become root node.'));\n this.draggedHover = false;\n this.service.draggedData = undefined;\n return;\n }\n\n super.drop($event);\n if (this.canDrop) {\n await this.moveNode(nodeToMove);\n } else {\n this.draggedHover = false;\n this.service.draggedData = undefined;\n }\n }\n\n hookEvents() {\n if (!this.events) {\n this.events = new Subject();\n this.events.subscribe(evt => {\n if (!this.loading) {\n this.handleEvent(evt);\n }\n });\n }\n }\n\n toString() {\n return AssetNode.NAME;\n }\n\n /**\n * Checks if the current node has child devices.\n */\n hasChildDevices() {\n return this.mo && this.mo.c8y_IsDevice && this.mo.childDevices.references.length > 0;\n }\n\n protected fetch() {\n return this.root\n ? this.service.getRootNodes()\n : this.service.getGroupItems(\n this.mo.id,\n this.hideDevices\n ? {\n query: `$filter=(has(${GroupFragment.groupFragmentType}))$orderby=name`\n }\n : {}\n );\n }\n\n protected async updateIcon(open) {\n this.icon = await this.service.icon(\n // if it's root we are going to pass a fake mo to get the same icon as groups\n this.root ? { c8y_IsDeviceGroup: {} } : this.mo,\n open\n );\n }\n\n protected countChildren() {\n return this.children.length;\n }\n\n protected async handleEvent(evt: AssetsNavigatorAction) {\n if (!this.countChildren() && evt === AssetsNavigatorAction.FETCH) {\n this.loading = true;\n this.addNodes(await this.fetch());\n this.loading = false;\n } else if (evt === AssetsNavigatorAction.NEXT) {\n this.loadMoreNode.loading = true;\n this.addNodes(await this.paging.next());\n this.loadMoreNode.loading = false;\n } else if (evt === AssetsNavigatorAction.REFRESH) {\n this.loading = false;\n this.paging = undefined;\n this.loadMoreNode = undefined;\n this.empty();\n this.events.next(AssetsNavigatorAction.FETCH);\n }\n }\n\n protected addNodes(res) {\n if (res.paging) {\n const { currentPage, nextPage, pageSize } = (this.paging = res.paging);\n if (currentPage === 1) {\n this.empty();\n }\n const itemsCount = res.data.length;\n const moreItemsAvailable = !!nextPage && itemsCount === pageSize;\n this.toggleLoadMore(moreItemsAvailable);\n }\n (res.data || res).map(mo => {\n return this.addManagedObject(mo);\n });\n this.events.next(AssetsNavigatorAction.LOADING_DONE);\n this.nodesFetched.next();\n }\n\n protected toggleLoadMore(show: boolean) {\n if (!this.loadMoreNode && show) {\n this.loadMoreNode = new LoadMoreNode();\n this.add(this.loadMoreNode);\n this.loadMoreNode.click = debounce(() => this.events.next(AssetsNavigatorAction.NEXT), 300, {\n leading: true,\n trailing: false\n });\n }\n\n if (this.loadMoreNode) {\n this.loadMoreNode.hidden = !show;\n }\n }\n\n private async moveNode(nodeToMove: AssetNode) {\n try {\n const isCopy = await this.showDropConfirm(nodeToMove);\n await this.verifyNodeAccess(nodeToMove);\n await this.addMovedNode(nodeToMove);\n if (!isCopy) {\n await this.removeMovedNode(nodeToMove);\n }\n this.expand();\n } catch (ex) {\n if (ex) {\n this.service.alert.addServerFailure(ex);\n }\n } finally {\n this.draggedHover = false;\n this.service.draggedData = undefined;\n }\n }\n\n private async showDropConfirm(nodeToMove: AssetNode) {\n this.confirm.title = gettext('Move');\n this.confirm.message = gettext('Do you want to move the group?');\n const buttons: any = [\n {\n label: gettext('Cancel'),\n action: () => Promise.reject()\n },\n {\n label: gettext('Move'),\n status: 'default',\n action: () => Promise.resolve(false)\n }\n ];\n if (nodeToMove.isDeviceOrProbablyChildDevice) {\n this.confirm.title = gettext('Move or add');\n this.confirm.message = gettext('Do you want to move or add the device?');\n buttons.push({\n label: gettext('Add'),\n status: 'primary',\n action: () => Promise.resolve(true)\n });\n }\n return this.confirm.show(buttons);\n }\n\n private async verifyNodeAccess(nodeToMove: AssetNode) {\n return this.service.inventory.update({ id: nodeToMove.mo.id });\n }\n\n private async addMovedNode(nodeToMove: AssetNode) {\n let mo: IIdentified;\n\n if (this.root && !this.isAsset(nodeToMove)) {\n mo = (\n await this.service.inventory.update({\n id: nodeToMove.mo.id,\n type: GroupFragment.groupType\n })\n ).data;\n\n this.addManagedObject(mo);\n return;\n }\n\n mo = (await this.service.inventory.childAssetsAdd(nodeToMove.mo, this.mo)).data;\n this.addManagedObject(mo);\n }\n\n private isAsset(nodeToMove: AssetNode) {\n // TODO use isAsset check when https://github.com/Cumulocity-IoT/cumulocity-ui/pull/690 is merged.\n // Do not override asset type!\n return nodeToMove.mo?.c8y_IsAsset;\n }\n\n private async removeMovedNode(nodeToMove: AssetNode) {\n for (const parent of nodeToMove.parents as AssetNode[]) {\n if (parent.mo && parent.mo.type === GroupFragment.dynamicGroupType) {\n break; // smart groups don't need to be changed\n }\n\n if (parent.root && !this.isAsset(nodeToMove)) {\n await this.service.inventory.update({\n id: nodeToMove.mo.id,\n type: GroupFragment.subGroupType\n });\n }\n\n if (!parent.root) {\n await this.service.inventory.childAssetsRemove(nodeToMove.mo, parent.mo);\n }\n parent.remove(nodeToMove);\n }\n }\n}\n","import { AssetNode } from './asset-node';\nimport { AssetNodeService } from './asset-node.service';\n\nexport class DynamicGroupNode extends AssetNode {\n constructor(protected service: AssetNodeService, config = {}) {\n super(service, config);\n this.draggable = false;\n this.droppable = false;\n }\n\n get hasChildren() {\n return true;\n }\n\n get query() {\n return this.mo.c8y_DeviceQueryString;\n }\n\n protected fetch(): any {\n return this.service.getDynamicGroupItems(this.query);\n }\n}\n","import { InjectionToken } from '@angular/core';\r\nimport { NavigatorNode } from '@c8y/ngx-components';\r\nimport { AssetNode } from './asset-node';\r\nexport const ASSET_NAVIGATOR_CONFIG = new InjectionToken<AssetNavigatorConfig>('AssetNodeConfig');\r\n\r\n/**\r\n * Configuration object of the AssetsNavigatorModule.\r\n */\r\nexport interface AssetNavigatorConfig {\r\n /**\r\n * Allows to enable smart groups in the module.\r\n * Default value: false.\r\n */\r\n smartGroups?: boolean;\r\n /**\r\n * Expands the root navigator node when the navigator is initialized.\r\n * Default value: false\r\n */\r\n openOnStart?: boolean;\r\n /**\r\n * Allows to change the position of the root group in the navigator.\r\n * The higher the value, the higher the position in navigation.\r\n * Default value: 2000.\r\n */\r\n rootNodePriority?: number;\r\n /**\r\n * Allows to drag and drop items in the asset navigator.\r\n * Default value: true.\r\n */\r\n disableDragAndDrop?: boolean;\r\n /**\r\n * Allows to override properies of the root group or hide it.\r\n * Setup examples:\r\n * - `rootNavigatorNode: true` - shows the default navigator node (default),\r\n * - `rootNavigatorNode: false` - hides the navigator node,\r\n * - `rootNavigatorNode: { label: 'New name', ... }` - overrides default navigator node,\r\n * - `rootNavigatorNode: new NavigatorNode({ label: 'New name', ... })` - overrides default navigator node,\r\n */\r\n rootNavigatorNode?: boolean | Partial<NavigatorNode | AssetNode>;\r\n}\r\n","import { Injectable } from '@angular/core';\nimport { IManagedObject } from '@c8y/client';\nimport { GroupService as MigratedDeviceGroupService } from '@c8y/ngx-components';\n\n@Injectable({\n providedIn: 'root'\n})\n\n/**\n * @deprecated\n * Original service was moved to core/common.\n * This service is deprecated and serves as a proxy to maintain backward compatibility.\n */\nexport class DeviceGroupService {\n constructor(private migratedDeviceGroupService: MigratedDeviceGroupService) {}\n\n icon(mo: IManagedObject, open = false): Promise<string> {\n return this.migratedDeviceGroupService.getIcon(mo, open);\n }\n\n isGroup(mo: IManagedObject): boolean {\n return this.migratedDeviceGroupService.isGroup(mo);\n }\n\n isDynamicGroup(mo: IManagedObject): boolean {\n return this.migratedDeviceGroupService.isDynamicGroup(mo);\n }\n\n isDataBroker(mo: IManagedObject): boolean {\n return this.migratedDeviceGroupService.isDataBroker(mo);\n }\n\n isDataBrokerActive(mo: IManagedObject) {\n return this.migratedDeviceGroupService.isDataBrokerActive(mo);\n }\n\n isAsset(mo: IManagedObject) {\n return this.migratedDeviceGroupService.isAsset(mo);\n }\n\n isAnyGroup(mo: IManagedObject) {\n return this.migratedDeviceGroupService.isAnyGroup(mo);\n }\n\n isDevice(mo: IManagedObject) {\n return this.migratedDeviceGroupService.isDevice(mo);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { ActivatedRouteSnapshot, ActivationEnd, Router } from '@angular/router';\nimport { IManagedObject, IResult, InventoryService, QueriesUtil, UserService } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n AlertService,\n AppStateService,\n BreadcrumbService,\n GroupFragment,\n ModalService,\n NavigatorNode,\n NavigatorNodeData,\n OptionsService,\n ViewContext,\n Permissions\n} from '@c8y/ngx-components';\nimport { ApiService } from '@c8y/ngx-components/api';\nimport { empty } from 'rxjs';\nimport { filter, first, mergeMap } from 'rxjs/operators';\nimport { AssetsNavigatorAction } from './action.enum';\nimport { AssetNode } from './asset-node';\nimport { ASSET_NAVIGATOR_CONFIG, AssetNavigatorConfig } from './asset-node-config.model';\nimport { DynamicGroupNode } from './dynamic-group-node';\nimport { DeviceGroupService } from './group.service';\nimport { TranslateService } from '@ngx-translate/core';\n\nexport interface AssetNodeMo {\n id: string;\n type: string;\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class AssetNodeService {\n rootNode: AssetNode;\n firstUrl = true;\n draggedData: AssetNode;\n queriesUtil: QueriesUtil;\n protected PAGE_SIZE = 20;\n\n inventory = inject(InventoryService);\n apiService = inject(ApiService);\n modal = inject(ModalService);\n alert = inject(AlertService);\n translateService = inject(TranslateService);\n\n protected breadcrumbService = inject(BreadcrumbService);\n protected user = inject(UserService);\n protected appState = inject(AppStateService);\n protected optionsService = inject(OptionsService);\n moduleConfig: AssetNavigatorConfig;\n protected deviceGroupService = inject(DeviceGroupService);\n protected router = inject(Router);\n\n constructor() {\n this.moduleConfig = {\n rootNodePriority: 2000,\n ...(inject(ASSET_NAVIGATOR_CONFIG, { optional: true }) || {})\n };\n this.queriesUtil = new QueriesUtil();\n\n this.router.events\n .pipe(\n filter(\n (event: ActivationEnd) =>\n event instanceof ActivationEnd && event.snapshot.data?.contextData\n ),\n first()\n )\n .subscribe(({ snapshot }) => {\n this.expandNodesOnStart(snapshot);\n });\n }\n\n /**\n * Expands the navigator nodes on first navigation.\n * @param snapshot The current navigation snapshot.\n */\n async expandNodesOnStart(snapshot: ActivatedRouteSnapshot): Promise<void> {\n if (!this.rootNode) {\n return;\n }\n if (\n snapshot.data?.context === ViewContext.Group ||\n snapshot.data?.context === ViewContext.Device\n ) {\n const { data } = await this.inventory.detail(snapshot.data.contextData, {\n withParents: true\n });\n\n const allManagedObjectParentIds = [\n ...data.assetParents.references.map(({ managedObject }) => {\n return managedObject.id;\n }),\n data.id\n ];\n this.expandAll(this.rootNode, allManagedObjectParentIds);\n return;\n }\n\n if (this.moduleConfig.openOnStart) {\n this.expandAll(this.rootNode, []);\n }\n }\n\n /**\n * Expands all the given ids recursively. Stops if it does not find any.\n * @param node The node where the expanding should be started\n * @param ids The ids that should be expanded.\n */\n expandAll(node: AssetNode, ids: string[]) {\n node.open = true;\n node.click({ open: true });\n if (node.events) {\n node.events\n .pipe(\n filter(action => action === AssetsNavigatorAction.LOADING_DONE),\n first()\n )\n .subscribe(() => {\n const nodeToExpand = node.children.find(\n childNode => childNode.mo?.id && ids.includes(childNode.mo.id)\n );\n if (nodeToExpand) {\n this.expandAll(\n nodeToExpand,\n ids.filter(id => id !== nodeToExpand.mo.id)\n );\n }\n });\n }\n }\n\n label(mo: IManagedObject): string {\n return (\n mo.name ||\n (this.isDevice(mo) &&\n this.translateService.instant(gettext('Device {{id}}'), { id: mo.id })) ||\n '--'\n );\n }\n\n icon(mo: IManagedObject, open?: boolean): Promise<string> {\n return this.deviceGroupService.icon(mo, open);\n }\n\n isGroup(mo: IManagedObject) {\n return this.deviceGroupService.isGroup(mo);\n }\n\n isDynamicGroup(mo: IManagedObject) {\n return this.deviceGroupService.isDynamicGroup(mo);\n }\n\n isDataBroker(mo: IManagedObject) {\n return this.deviceGroupService.isDataBroker(mo);\n }\n\n isDataBrokerActive(mo: IManagedObject) {\n return this.deviceGroupService.isDataBrokerActive(mo);\n }\n\n isAsset(mo: IManagedObject) {\n return this.deviceGroupService.isAsset(mo);\n }\n\n isAnyGroup(mo: IManagedObject) {\n return this.deviceGroupService.isAnyGroup(mo);\n }\n\n isDevice(mo: IManagedObject) {\n return this.deviceGroupService.isDevice(mo);\n }\n\n createRootNode(config: NavigatorNodeData = {}) {\n this.rootNode = this.createAssetNode({\n root: true,\n ...config,\n priority: this.moduleConfig.rootNodePriority,\n featureId: 'groups'\n });\n return this.rootNode;\n }\n\n createDynamicGroupNode(config) {\n return new DynamicGroupNode(this, config);\n }\n\n createAssetNode(config: Partial<AssetNode>) {\n return new AssetNode(this, config);\n }\n\n createChildNode(managedObject, config: Partial<AssetNode>) {\n const { type } = managedObject;\n config.mo = managedObject;\n if (type === GroupFragment.dynamicGroupType) {\n return this.createDynamicGroupNode(config);\n }\n return this.createAssetNode(config);\n }\n\n getRootNodes(customFilter?: any): Promise<any> {\n const defaultFilter = {\n pageSize: this.PAGE_SIZE,\n withChildren: false,\n onlyRoots: !this.optionsService.disableOnlyRootsQuery,\n query: this.queriesUtil.buildQuery(this.navRootQueryFilter())\n };\n const groupFilter = { ...defaultFilter, ...customFilter };\n\n // due to BE performance limitations we do not allow filtering and sorting for a user without inventory roles\n if (\n this.appState?.currentUser?.value &&\n !this.user.hasAnyRole(this.appState.currentUser.value, [\n Permissions.ROLE_INVENTORY_READ,\n Permissions.ROLE_MANAGED_OBJECT_READ\n ])\n ) {\n delete groupFilter.query;\n Object.assign(groupFilter, {\n fragmentType: GroupFragment.groupFragmentType,\n onlyRoots: true\n });\n }\n return this.inventory.list(this.createFilter(groupFilter));\n }\n\n getAllInventories(customFilter?: any): Promise<any> {\n const defaultFilter = {\n pageSize: this.PAGE_SIZE,\n withChildren: false\n };\n const groupFilter = { ...defaultFilter, ...customFilter };\n return this.inventory.list(this.createFilter(groupFilter));\n }\n\n getGroupItems(moId: string, extraFilter: object = {}, withChildren = false, filterQuery = '') {\n const queryFilter = {\n withChildren,\n pageSize: this.PAGE_SIZE,\n query: this.groupQueryFilter(moId, filterQuery)\n };\n return this.inventory.childAssetsList(moId, { ...queryFilter, ...extraFilter });\n }\n\n getUnassignedDevices(withChildren = false, filterQuery = '') {\n const queryFilter: any = {\n fragmentType: 'c8y_IsDevice',\n onlyRoots: true,\n withChildren,\n pageSize: this.PAGE_SIZE,\n q: this.getUnassignedDevicesQueryStr(filterQuery)\n };\n return this.inventory.list(this.createFilter(queryFilter));\n }\n\n getDynamicGroupItems(groupQuery: string, filterObj: any = {}) {\n const { query, ...queryParams } = filterObj;\n const orderByQuery = query;\n const queryFilter = {\n q: this.buildCombinedQuery(groupQuery, orderByQuery),\n ...queryParams\n };\n return this.inventory.list(this.createFilter(queryFilter));\n }\n\n getDeviceChildren(\n moId: string,\n extraFilter: object = {},\n filterQuery = '',\n withChildren = false\n ) {\n const queryFilter = {\n withChildren,\n pageSize: this.PAGE_SIZE,\n query: this.groupQueryFilter(moId, filterQuery)\n };\n return this.inventory.childDevicesList(moId, { ...queryFilter, ...extraFilter });\n }\n\n getUnassignedDevicesQueryStr(filterQuery): string {\n const hasGroupId = filterQuery.includes('bygroupid');\n // Fetch all unassigned devices.\n const defaultQueryStr = '$orderby=name';\n\n // filterQuery is a custom query to fetch unassigned devices filtered by name.\n return hasGroupId || !filterQuery ? defaultQueryStr : filterQuery;\n }\n\n groupQueryFilter(moId: string, filterQuery?: string) {\n if (!filterQuery) {\n return `$filter=(bygroupid(${moId}))$orderby=name`;\n }\n return filterQuery;\n }\n\n navRootQueryFilter() {\n const navRootFilter = this.rootQueryFilter();\n navRootFilter.__orderby = [{ name: 1 }];\n return navRootFilter;\n }\n\n rootQueryFilter() {\n const { moduleConfig } = this;\n const rootFilter = this.optionsService.disableOnlyRootsQuery\n ? {\n __filter: {\n type: GroupFragment.groupType\n },\n __orderby: []\n }\n : {\n __filter: {\n __has: GroupFragment.groupFragmentType\n },\n __orderby: []\n };\n if (moduleConfig.smartGroups) {\n const queryFilter = {\n __filter: {\n __and: [\n {\n type: GroupFragment.dynamicGroupType\n },\n {\n __has: GroupFragment.dynamicGroupFragment\n },\n { __not: { __has: `${GroupFragment.dynamicGroupFragment}.invisible` } }\n ]\n }\n };\n this.queriesUtil.addOrFilter(rootFilter, queryFilter);\n }\n return rootFilter;\n }\n\n onUpdate({ mo, root }) {\n if (mo.id) {\n return this.apiService\n .hookResponse(\n ({ url, method }) =>\n ['PUT', 'DELETE', 'POST'].includes(method) &&\n RegExp(`inventory/managedObjects/${mo.id}`).test(url)\n )\n .pipe(\n filter(() => !this.draggedData),\n this.apiService.excludePermissionCall(),\n mergeMap(this.apiService.resolveData<IManagedObject>),\n filter(response => !response?.data?.c8y_Dashboard)\n );\n } else if (root) {\n return this.apiService\n .hookResponse(\n ({ url, method }) => RegExp('inventory/managedObjects/?$').test(url) && method === 'POST'\n )\n .pipe(\n mergeMap(this.apiService.resolveData<IManagedObject>),\n filter(response => this.isNewManagedObjectRoot(response))\n );\n } else {\n return empty();\n }\n }\n\n isNewManagedObjectRoot(response: Partial<IResult<IManagedObject>> = {}) {\n const { data } = response;\n let isRootAsset = false;\n\n if (typeof data === 'object') {\n isRootAsset = !!data[GroupFragment.groupFragmentType];\n if (!isRootAsset && this.moduleConfig.smartGroups) {\n isRootAsset = !!data[GroupFragment.dynamicGroupFragment];\n }\n }\n return isRootAsset;\n }\n\n /**\n * Check if it is possible to drop a node after dragging.\n * @param dropOnRoot Is the drop performed on the root node\n */\n canDropNode(dropOnRoot: boolean): boolean {\n return (\n !dropOnRoot ||\n this.user.hasAnyRole(this.appState.currentUser.value, [\n Permissions.ROLE_INVENTORY_ADMIN,\n Permissions.ROLE_MANAGED_OBJECT_ADMIN\n ])\n );\n }\n\n /**\n * There could be multiple breadcrumbs for devices,\n * so we set a preferred one on click on a device.\n * @param parents The parent nodes of the device to select the prefered one.\n */\n preferBreadcrumb(parents: NavigatorNode[]) {\n if (parents.length === 1) {\n this.breadcrumbService.selectPreferredByPath(parents[0].path);\n }\n }\n\n protected createFilter(extraParams: any = {}) {\n const params = {\n currentPage: 1,\n withTotalPages: true,\n pageSize: 10\n };\n return { ...params, ...extraParams };\n }\n\n private buildCombinedQuery(queryA, queryB) {\n let combinedQuery;\n if (queryA && queryB) {\n const filterQuery = this.queriesUtil.buildQuery([\n {\n __useFilterQueryString: queryA\n },\n {\n __useFilterQueryString: queryB\n }\n ]);\n const orberByQuery = this.queriesUtil.extractAndMergeOrderBys([queryA, queryB]);\n combinedQuery = `${filterQuery} ${orberByQuery}`;\n } else {\n combinedQuery = queryA || queryB || '';\n }\n return combinedQuery;\n }\n}\n","import { Inject, Injectable, Optional } from '@angular/core';\nimport { get as getProp } from 'lodash-es';\nimport { AssetNode } from './asset-node';\nimport { AssetNavigatorConfig, ASSET_NAVIGATOR_CONFIG } from './asset-node-config.model';\nimport { AssetNodeService } from './asset-node.service';\n\n@Injectable()\nexport class AssetNodeFactory {\n constructor(\n private service: AssetNodeService,\n @Optional() @Inject(ASSET_NAVIGATOR_CONFIG) public moduleConfig: AssetNavigatorConfig\n ) {}\n\n get(): AssetNode {\n const rootNavigatorNode = getProp(this.moduleConfig, 'rootNavigatorNode') ?? true;\n let { rootNode } = this.service;\n\n if (rootNavigatorNode === false) {\n return;\n }\n\n if (!rootNode) {\n rootNode = this.service.createRootNode(rootNavigatorNode === true ? {} : rootNavigatorNode);\n }\n return rootNode;\n }\n}\n","import { GroupFragment } from '@c8y/ngx-components';\nimport { GroupNodeService } from './group-node.service';\nimport { AssetNode } from '../asset-node';\nimport { GroupNodeConfig } from './asset-selector.model';\n\nexport class GroupNode extends AssetNode {\n static NAME = 'GroupNode';\n\n /**\n * Set this true, if only groups should be shown.\n */\n groupsOnly = false;\n\n /**\n * Set this true, if it groups are also selectable.\n */\n groupsSelectable = false;\n /**\n * Devices with children can be selected to show their child devices.\n */\n showChildDevices = false;\n\n /**\n * Group node children (subentries).\n */\n children: GroupNode[] = [];\n\n /**\n * Creates a new node which shows only groups.\n *\n * @param service The service to use.\n * @param config The default configuration of the node.\n * @param groupsOnly Set this true, if only groups should be shown.\n * @param selectable Set this true, if it is selectable.\n */\n\n constructor(protected service: GroupNodeService, config: GroupNodeConfig = {}) {\n super(service, config);\n this.groupsOnly = config.groupsOnly || false;\n this.groupsSelectable = config.groupsSelectable || false;\n this.showChildDevices = config.showChildDevices || false;\n }\n\n /**\n * Adds the MO as a child node.\n * @param mo ManagedObject\n */\n addManagedObject(mo) {\n const { childAdditions } = this.mo;\n if (!this.isChildAddition(childAdditions, mo)) {\n this.add(\n this.service.createChildNode({\n mo,\n groupsOnly: this.groupsOnly,\n groupsSelectable: this.groupsSelectable,\n showChildDevices: this.showChildDevices\n })\n );\n }\n }\n\n /**\n * Counts the number of children for the current node (with the exception of the UnassignedDevicesNode).\n */\n countChildren(): number {\n return this.children.filter(value => value.toString() !== 'UnassignedDevicesNode').length;\n }\n\n /**\n * Removes all child nodes except the UnassignedDevicesNode.\n */\n empty() {\n this.children = this.children.filter(value => value.toString() === 'UnassignedDevicesNode');\n }\n\n fetch() {\n const isRoot = this.root;\n const isDevice = this.mo.c8y_IsDevice;\n return isRoot\n ? this.service.getRootNodes()\n : isDevice\n ? this.service.getDeviceChildren(\n this.mo.id,\n {},\n this.filterQuery$.value,\n this.showChildDevices\n )\n : this.service.getGroupItems(\n this.mo.id,\n this.groupsOnly\n ? {\n query: `$filter=(has(${GroupFragment.groupFragmentType}))`\n }\n : {},\n this.showChildDevices,\n this.filterQuery$.value\n );\n }\n\n toString() {\n return GroupNode.NAME;\n }\n\n isGroup() {\n return this.mo && this.service.isGroup(this.mo);\n }\n}\n","import { Injectable } from '@angular/core';\nimport { AssetNodeService } from '../asset-node.service';\nimport { GroupNodeConfig } from './asset-selector.model';\nimport { GroupNode } from './group-node';\n\n@Injectable({ providedIn: 'root' })\nexport class GroupNodeService extends AssetNodeService {\n protected PAGE_SIZE = 5;\n\n createGroupNode(config: GroupNodeConfig) {\n return new GroupNode(this, config);\n }\n\n createChildNode(config: GroupNodeConfig): GroupNode {\n return this.createGroupNode(config);\n }\n}\n","import { Injectable } from '@angular/core';\nimport { IIdentified, IManagedObject } from '@c8y/client';\nimport { AssetNodeService } from '../asset-node.service';\nimport { isArray, isNumber, isObject, isString } from 'lodash';\nimport { GroupNode } from './group-node';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class AssetSelectorService extends AssetNodeService {\n /**\n * Function which will check if the node is selectable.\n */\n private isNodeSelectableFn: boolean | ((node: GroupNode) => boolean) = true;\n\n /**\n * Sets the function that will decide if the node is selectable.\n * @param fn A boolean or a function that will decide if the node is selectable.\n */\n setIsNodeSelectable(fn: boolean | ((node: GroupNode) => boolean)): void {\n this.isNodeSelectableFn = fn;\n }\n\n /**\n * Checks if the node is selectable.\n * @param node The node to check.\n */\n isNodeSelectable(node: GroupNode): boolean {\n if (typeof this.isNodeSelectableFn === 'boolean') {\n return this.isNodeSelectableFn;\n } else if (typeof this.isNodeSelectableFn === 'function') {\n return this.isNodeSelectableFn(node);\n }\n return true;\n }\n /**\n * Simplifies the object model based on the selected mode.\n * @param obj The selected asset.\n * @param mode The mode which will decide what type of model will be returned.\n */\n normalizeValue(\n obj: Partial<IManagedObject>,\n modelMode: 'simple' | 'full'\n ): Partial<IManagedObject> | IIdentified {\n return this.simplifyModel(this.normalizeModelValue(obj), modelMode);\n }\n\n simplifyModel(\n model: Partial<IManagedObject> | IIdentified[],\n mode: 'simple' | 'full'\n ): Partial<IManagedObject> | IIdentified {\n const mapModel = model => {\n const { id, name, c8y_DeviceQueryString } = model as IIdentified;\n return { id, name, ...(c8y_DeviceQueryString ? { c8y_DeviceQueryString } : {}) };\n };\n\n if (mode === 'full') {\n return model;\n }\n if (!isArray(model)) {\n return mapModel(model);\n }\n return model.map(mapModel);\n }\n\n /**\n * Returns the index of the currently selected item.\n * @param selected All selected items\n * @param selectedMo The new selected item-\n * @returns An index, or -1 if not found.\n */\n getIndexOfSelected(\n selected: Array<Partial<IIdentified>> | Partial<IIdentified>,\n selectedMo: IIdentified\n ) {\n return selected.findIndex(mo => mo.id === selectedMo.id);\n }\n\n private normalizeModelValue(value: any): IIdentified[] {\n if (isNumber(value) || isString(value)) {\n return [{ id: value }];\n }\n if (isArray(value)) {\n return value;\n }\n if (isObject(value)) {\n return [value];\n }\n return [];\n }\n}\n","import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';\nimport { IIdentified, IManagedObject } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { IconDirective, C8yTranslateDirective, C8yTranslatePipe } from '@c8y/ngx-components';\nimport { TranslateService } from '@ngx-translate/core';\nimport { Subject } from 'rxjs';\nimport { filter, takeUntil } from 'rxjs/operators';\nimport { AssetsNavigatorAction } from '../action.enum';\nimport { AssetSelectorService } from './asset-selector.service';\nimport { GroupNode } from './group-node';\nimport { NgIf, NgStyle, NgClass, NgFor } from '@angular/common';\nimport { CollapseDirective } from 'ngx-bootstrap/collapse';\n\n@Component({\n selector: 'c8y-asset-selector-node',\n templateUrl: './asset-selector-node.component.html',\n imports: [\n NgIf,\n NgStyle,\n NgClass,\n IconDirective,\n C8yTranslateDirective,\n CollapseDirective,\n NgFor,\n C8yTranslatePipe\n ]\n})\nexport class AssetSelectorNodeComponent implements OnInit {\n /**\n * The current node.\n */\n @Input() node: GroupNode;\n /**\n * Root node. Node found at the top of the hierarchy.\n */\n @Input() rootNode: GroupNode;\n /**\n * All preselected items.\n */\n @Input() preselected: IIdentified | IIdentified[] = [];\n /**\n * Should the path be shown.\n */\n @Input() showPath = false;\n /**\n * Can the user select multiple assets.\n */\n @Input() multi = false;\n /**\n * The current path to the node.\n */\n @Input() view: 'tree' | 'miller' = 'tree';\n /**\n * Used only for miller-view, displays the column level for the current node.\n * E.g if the index is one, this will be second column.\n */\n @Input() index;\n /**\n * Sets the active node.\n */\n @Input() active: GroupNode;\n /**\n * A function that should verify the need to add a column when a node is clicked.\n */\n @Input() handleNextMillerViewColumn: (node: GroupNode, index: number) => boolean;\n\n @Input() disabled = false;\n /**\n * Event, which indicates whether the loading of the node has completed.\n */\n @Output() isLoadingState: EventEmitter<boolean> = new EventEmitter();\n /**\n * Event that emits when a node is selected.\n */\n @Output() onSelect: EventEmitter<IManagedObject> = new EventEmitter();\n /**\n * Event that emits when a node is deselected.\n */\n @Output() onDeselect: EventEmitter<{ deselectMode: 'single' | 'all'; mo: IManagedObject }> =\n new EventEmitter();\n\n breadcrumb: string;\n /**\n * @ignore\n */\n level = 0;\n /**\n * @ignore\n */\n unsubscribe$ = new Subject<void>();\n isNodeSelectable = true;\n\n /** sets the `btn-pending` class in the load more button */\n isLoading = false;\n /**\n * @ignore\n */\n get expandTitle() {\n return !this.node.open ? gettext('Expand') : gettext('Collapse');\n }\n\n /**\n * @ignore only di\n */\n constructor(\n private translateService: TranslateService,\n private cd: ChangeDetectorRef,\n public assetSelectorService: AssetSelectorService\n ) {}\n\n /**\n * @ignore\n */\n async ngOnInit() {\n this.isNodeSelectable = this.assetSelectorService.isNodeSelectable(this.node);\n this.breadcrumb = this.node.label;\n this.setupBreadcrumbsAndLevel(this.node);\n\n if (this.node instanceof GroupNode) {\n this.node.hookEvents();\n }\n\n // open on startup\n if (this.node.root) {\n this.click();\n }\n\n // used for loading and to trigger change detection when the node is no longer loading.\n if (this.node.events) {\n this.node.events\n .pipe(\n takeUntil(this.unsubscribe$),\n filter((a: AssetsNavigatorAction) => a === AssetsNavigatorAction.LOADING_DONE)\n )\n .subscribe(() => {\n this.isLoadingState.emit(false);\n this.cd.markForCheck();\n });\n }\n }\n\n /**\n * Opens a node.\n */\n click() {\n this.node.open = !this.node.open;\n this.node.click({ open: this.node.open });\n }\n\n setupBreadcrumbsAndLevel(node: GroupNode) {\n if (node.parents && node.parents.length) {\n const parent = node.parents[0] as GroupNode;\n this.breadcrumb =\n this.translateService.instant(parent.label) +\n ' > ' +\n this.translateService.instant(this.breadcrumb);\n this.level++;\n\n this.setupBreadcrumbsAndLevel(parent);\n }\n }\n\n /**\n * Selects the node and emits a change on the parent component.\n * @param node The node to select.\n */\n selected(node: GroupNode) {\n if (node.mo) {\n this.updateSelection(node.mo);\n return;\n }\n this.click();\n }\n\n /**\n * Handles clicks on a item in Miller View.\n * @param node The node that was clicked.\n */\n millerViewClick(node: GroupNode) {\n node.breadcrumb = this.breadcrumb;\n\n if (!this.handleNextMillerViewColumn) {\n return;\n }\n const shouldHandleDefault = this.handleNextMillerViewColumn(node, this.index);\n if (shouldHandleDefault) {\n this.selected(node);\n }\n }\n\n /**\n * @ignore\n */\n ngOnDestroy(): void {\n this.unsubscribe$.next();\n this.unsubscribe$.complete();\n }\n\n isSelected() {\n if (!this.node.mo) {\n return false;\n }\n return this.assetSelectorService.getIndexOfSelected(this.preselected, this.node.mo) > -1;\n }\n\n isActive() {\n if (this.active && this.node.mo) {\n return this.active.mo?.id === this.node.mo.id;\n }\n return false;\n }\n\n private updateSelection(selectedMo: IManagedObject) {\n if (!this.multi) {\n this.onDeselect.emit({ deselectMode: 'all', mo: selectedMo });\n return;\n }\n if (this.isSelected()) {\n this.onDeselect.emit({ deselectMode: 'single', mo: selectedMo });\n return;\n }\n this.onSelect.emit(selectedMo);\n this.cd.markForCheck();\n }\n}\n","<!-- 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 rol