@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,114 lines (1,102 loc) • 114 kB
JavaScript
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"] }