@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,293 lines (1,288 loc) • 218 kB
JavaScript
import { gettext } from '@c8y/ngx-components/gettext';
import * as i0 from '@angular/core';
import { InjectionToken, inject, Injectable, Directive, Injector, runInInjectionContext, Pipe, viewChildren, ViewContainerRef, effect, reflectComponentType, Input, Component, viewChild, EventEmitter, ContentChild, Output, forwardRef } from '@angular/core';
import { InventoryService } from '@c8y/client';
import * as i2 from '@c8y/ngx-components';
import { hookGeneric, ExtensionPointForPlugins, GroupService, fromTriggerOnce, getInjectedHooks, stateToFactory, AssetTypesRealtimeService, AlertService, FeatureCacheService, AssetDefinitionsService, isPromise, IconDirective, C8yTranslatePipe, ListGroupModule, EmptyStateComponent, FormsModule as FormsModule$1, LoadMoreComponent, TabComponent, TabsOutletComponent, BottomDrawerRef, BottomDrawerService } from '@c8y/ngx-components';
import { isEqual, omit, isArray, isObjectLike, isEmpty, find, forOwn, get, isUndefined, cloneDeep } from 'lodash-es';
import { firstValueFrom, mergeMap, filter, take, map, distinctUntilChanged, shareReplay, BehaviorSubject, of, from, switchMap, isObservable, Subject, takeUntil, debounceTime } from 'rxjs';
import * as i1 from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { NgFor, NgClass, NgTemplateOutlet, AsyncPipe, JsonPipe } from '@angular/common';
import { CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
import * as i1$1 from '@angular/forms';
import { NgForm, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import * as i3 from 'ngx-bootstrap/tooltip';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import * as i4 from '@angular/cdk/tree';
import { CdkTreeModule } from '@angular/cdk/tree';
import { DataSource } from '@angular/cdk/collections';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { MillerViewComponent } from '@c8y/ngx-components/assets-navigator';
const defaultAssetPropertyListConfig = {
filterable: true,
selectMode: 'none',
expansionMode: 'expandedByDefault',
showHeader: true,
showValue: true,
showKey: true,
emptyStateContent: 'empty',
allowAddingCustomProperties: false,
inputPropertiesHandle: 'merge',
allowDragAndDrop: false
};
const defaultAssetProperties = [
{
c8y_JsonSchema: { properties: { name: { type: 'string', label: 'Name' } } },
name: 'name',
label: gettext('Name'),
type: 'string',
active: true,
isEditable: true,
isStandardProperty: true
},
{
c8y_JsonSchema: { properties: { id: { type: 'string', label: 'ID' } } },
name: 'id',
label: gettext('ID'),
type: 'string',
active: true,
isEditable: false,
isStandardProperty: true
},
{
c8y_JsonSchema: {
properties: { type: { type: 'string', label: 'Type' } }
},
name: 'type',
label: gettext('Type'),
type: 'string',
active: true,
isEditable: false,
isStandardProperty: true
},
{
c8y_JsonSchema: {
properties: { owner: { type: 'string', label: 'Owner' } }
},
name: 'owner',
label: gettext('Owner'),
type: 'string',
isEditable: false,
isStandardProperty: true
},
{
c8y_JsonSchema: {
properties: { lastUpdated: { type: 'string', label: 'Last updated' } }
},
name: 'lastUpdated',
label: gettext('Last updated'),
type: 'string',
isEditable: false,
isStandardProperty: true
},
{
label: gettext('Active alarms status'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_ActiveAlarmsStatus',
c8y_JsonSchema: {
properties: {
c8y_ActiveAlarmsStatus: {
key: 'c8y_ActiveAlarmsStatus',
type: 'object',
label: 'Active alarms status',
properties: {
critical: {
title: 'Critical',
type: 'number'
},
major: {
title: 'Major',
type: 'number'
},
minor: {
title: 'Minor',
type: 'number'
},
warning: {
title: 'Warning',
type: 'number'
}
}
}
}
}
},
{
label: gettext('Address'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Address',
c8y_JsonSchema: {
properties: {
c8y_Address: {
key: 'c8y_Address',
type: 'object',
label: 'Address',
properties: {
street: {
title: 'Street',
type: 'string'
},
city: {
title: 'City',
type: 'string'
},
cityCode: {
title: 'City code',
type: 'string'
},
territory: {
title: 'Territory',
type: 'string'
},
region: {
title: 'Region',
type: 'string'
},
country: {
title: 'Country',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Agent'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Agent',
c8y_JsonSchema: {
properties: {
c8y_Agent: {
key: 'c8y_Agent',
type: 'object',
label: 'Agent',
properties: {
name: {
title: 'Name',
type: 'string'
},
version: {
title: 'Version',
type: 'string'
},
url: {
title: 'URL',
type: 'string'
},
maintainer: {
title: 'Maintainer',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Availability'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Availability',
c8y_JsonSchema: {
properties: {
c8y_Availability: {
key: 'c8y_Availability',
type: 'object',
label: 'Availability',
properties: {
status: {
title: 'Status',
type: 'string'
},
lastMessage: {
title: 'Last message',
type: 'string',
printFormat: 'datetime'
}
}
}
}
}
},
{
label: gettext('Connection'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Connection',
c8y_JsonSchema: {
properties: {
c8y_Connection: {
key: 'c8y_Connection',
type: 'object',
label: 'Connection',
properties: {
status: {
title: 'Status',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Communication mode'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_CommunicationMode',
c8y_JsonSchema: {
properties: {
c8y_CommunicationMode: {
key: 'c8y_CommunicationMode',
type: 'object',
label: 'Communication mode',
properties: {
mode: {
title: 'Mode',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Firmware'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Firmware',
c8y_JsonSchema: {
properties: {
c8y_Firmware: {
key: 'c8y_Firmware',
type: 'object',
label: 'Firmware',
properties: {
moduleVersion: {
title: 'Module version',
type: 'string'
},
name: {
title: 'Name',
type: 'string'
},
version: {
title: 'Version',
type: 'string'
},
url: {
title: 'URL',
type: ['string', 'null']
}
}
}
}
}
},
{
label: gettext('Hardware'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Hardware',
c8y_JsonSchema: {
properties: {
c8y_Hardware: {
key: 'c8y_Hardware',
type: 'object',
label: 'Hardware',
properties: {
model: {
title: 'Model',
type: 'string'
},
serialNumber: {
title: 'Serial number',
type: 'string'
},
revision: {
title: 'Revision',
type: 'string'
}
}
}
}
}
},
{
label: gettext('LPWAN device'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_LpwanDevice',
c8y_JsonSchema: {
properties: {
c8y_LpwanDevice: {
key: 'c8y_LpwanDevice',
type: 'object',
label: 'LPWAN device',
properties: {
provisioned: {
title: 'Provisioned',
type: 'boolean'
}
}
}
}
}
},
{
label: gettext('Mobile'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Mobile',
c8y_JsonSchema: {
properties: {
c8y_Mobile: {
key: 'c8y_Mobile',
type: 'object',
label: 'Mobile',
properties: {
cellId: {
title: 'Cell ID',
type: ['string', 'null']
},
connType: {
title: 'Connection type',
type: 'string',
readOnly: true
},
currentOperator: {
title: 'Current operator',
type: 'string',
readOnly: true
},
currentBand: {
title: 'Current band',
type: 'string',
readOnly: true
},
ecn0: {
title: 'ECN0',
type: 'string',
readOnly: true
},
iccid: {
title: 'ICCID',
type: ['string', 'null']
},
imei: {
title: 'IMEI',
type: ['string', 'null']
},
imsi: {
title: 'IMSI',
type: ['string', 'null']
},
lac: {
title: 'LAC',
type: ['string', 'null']
},
mcc: {
title: 'MCC',
type: ['string', 'null']
},
mnc: {
title: 'MNC',
type: ['string', 'null']
},
msisdn: {
title: 'MSISDN',
type: 'string'
},
rcsp: {
title: 'RCSP',
type: 'string',
readOnly: true
},
rscp: {
title: 'RSCP',
type: 'string',
readOnly: true
},
rsrp: {
title: 'RSRP',
type: 'string',
readOnly: true
},
rsrq: {
title: 'RSRQ',
type: 'string',
readOnly: true
},
rssi: {
title: 'RSSI',
type: 'string',
readOnly: true
}
}
}
}
}
},
{
name: 'c8y_Notes',
label: 'Notes',
type: 'string',
isEditable: true,
isStandardProperty: true,
c8y_JsonSchema: {
properties: {
c8y_Notes: {
type: 'string',
label: 'Notes',
'x-schema-form': {
type: 'textarea'
}
}
}
}
},
{
label: gettext('Position'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Position',
c8y_JsonSchema: {
properties: {
c8y_Position: {
key: 'c8y_Position',
type: 'object',
label: 'Position',
properties: {
lat: {
title: 'Latitude',
type: 'number'
},
lng: {
title: 'Longitude',
type: 'number'
},
alt: {
title: 'Altitude',
type: 'number'
}
}
}
}
}
},
{
label: gettext('Required availability'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_RequiredAvailability',
c8y_JsonSchema: {
properties: {
c8y_RequiredAvailability: {
key: 'c8y_RequiredAvailability',
type: 'object',
label: 'Required availability',
properties: {
responseInterval: {
title: 'Response interval',
description: 'Takes a value between -32768 and 32767 minutes (a negative value indicates that the device is under maintenance).',
type: 'integer',
minimum: -32768,
maximum: 32767
}
}
}
}
}
},
{
label: gettext('Software'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Software',
c8y_JsonSchema: {
properties: {
c8y_Software: {
key: 'c8y_Software',
type: 'object',
label: 'Software',
properties: {
name: {
title: 'Name',
type: 'string'
},
version: {
title: 'Version',
type: 'string'
},
url: {
title: 'URL',
type: ['string', 'null']
}
}
}
}
}
},
{
label: gettext('Network'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Network',
c8y_JsonSchema: {
properties: {
c8y_Network: {
key: 'c8y_Network',
type: 'object',
label: 'Network',
properties: {
c8y_DHCP: {
title: 'DHCP',
type: 'object',
printFormat: 'hidden',
name: 'c8y_DHCP',
properties: {
addressRange: {
title: 'Address range',
type: 'object',
name: 'addressRange',
printFormat: 'hidden',
properties: {
start: {
title: 'Start',
type: 'string'
},
end: {
title: 'End',
type: 'string'
}
}
},
dns1: {
title: 'DNS 1',
type: 'string'
},
dns2: {
title: 'DNS 2',
type: 'string'
},
enabled: {
title: 'Enabled',
type: 'integer'
}
}
},
c8y_LAN: {
title: 'LAN',
type: 'object',
name: 'c8y_LAN',
printFormat: 'hidden',
properties: {
enabled: {
title: 'Enabled',
type: 'integer'
},
ip: {
title: 'IP',
type: 'string'
},
mac: {
title: 'MAC',
type: 'string'
},
name: {
title: 'Name',
type: 'string'
},
netmask: {
title: 'Netmask',
type: 'string'
}
}
},
c8y_WAN: {
title: 'WAN',
type: 'object',
name: 'c8y_WAN',
printFormat: 'hidden',
properties: {
apn: {
title: 'APN',
type: 'string'
},
authType: {
title: 'Auth type',
type: 'string'
},
ip: {
title: 'IP',
type: 'string'
},
password: {
title: 'Password',
type: 'string'
},
simStatus: {
title: 'SIM status',
type: 'string'
},
username: {
title: 'Username',
type: 'string'
}
}
}
}
}
}
}
}
];
const RESULT_TYPES = {
VALUE: { name: 'VALUE', value: 1, label: gettext('Only value') },
VALUE_UNIT: { name: 'VALUE_UNIT', value: 2, label: gettext('Value and unit') },
VALUE_UNIT_TIME: { name: 'VALUE_UNIT_TIME', value: 3, label: gettext('Value, unit and time') }
};
const HOOK_COMPUTED_PROPERTY = new InjectionToken('HOOK_COMPUTED_PROPERTY');
function hookComputedProperty(property, options) {
return hookGeneric(property, HOOK_COMPUTED_PROPERTY, options);
}
class ComputedPropertiesService extends ExtensionPointForPlugins {
constructor(rootInjector, router, plugins) {
super(rootInjector, plugins);
this.router = router;
this.groupService = inject(GroupService);
this.items$ = this.setupItemsObservable();
}
/**
* Returns the current state.
* @readonly
* @returns The current set of computed properties.
*/
get state() {
return this.state$.value;
}
add(propertyDef) {
this.state.add(propertyDef);
this.emitNewState();
}
getByName(name) {
if (!name) {
return Promise.resolve(undefined);
}
return firstValueFrom(this.items$.pipe(mergeMap((propertyDefs) => propertyDefs), filter((propertyDef) => propertyDef.name === name), take(1)));
}
getByContext(asset) {
if (!asset) {
return Promise.resolve([]);
}
const computedPropertyContextType = this.getTypeOfContext(asset);
return firstValueFrom(this.items$.pipe(map((propertyDefs) => propertyDefs.filter((propertyDef) => {
if (!propertyDef.contextType) {
return true;
}
return propertyDef.contextType.includes(computedPropertyContextType);
}))));
}
getTypeOfContext(context) {
const isEvent = (item) => !('severity' in item);
const isAlarm = (item) => 'severity' in item;
if (this.groupService.isDevice(context)) {
return 'device';
}
else if (this.groupService.isGroup(context) &&
!this.groupService.isAsset(context)) {
return 'group';
}
else if (this.groupService.isAsset(context)) {
return 'asset';
}
else if (isAlarm(context)) {
return 'alarm';
}
else if (isEvent(context)) {
return 'event';
}
}
setupItemsObservable() {
return fromTriggerOnce(this.router, this.refresh$, [
getInjectedHooks(HOOK_COMPUTED_PROPERTY, this.injectors),
() => this.factories,
stateToFactory(this.state$)
]).pipe(distinctUntilChanged(), shareReplay(1));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ComputedPropertiesService, deps: [{ token: i0.Injector }, { token: i1.Router }, { token: i2.PluginsResolveService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ComputedPropertiesService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ComputedPropertiesService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i0.Injector }, { type: i1.Router }, { type: i2.PluginsResolveService }] });
/**
* Service for managing asset properties.
*/
class AssetPropertiesService {
constructor() {
this.FRAGMENTS_TO_OMIT = [
'additionParents',
'assetParents',
'deviceParents',
'childAdditions',
'childAssets',
'childDevices',
'c8y_IsDevice',
'__children',
'c8y_ui',
'self',
'parent',
'c8y_DataPoint',
'c8y_Kpi_Migrated',
/^c8y_Dashboard!\d+/
];
this.MODEL_API_FEATURE_KEY = 'dtm.asset-api';
this.inventoryService = inject(InventoryService);
this.assetTypesRealtimeService = inject(AssetTypesRealtimeService);
this.groupService = inject(GroupService);
this.alert = inject(AlertService);
this.computedPropertiesService = inject(ComputedPropertiesService);
this.featureCacheService = inject(FeatureCacheService);
this.assetDefinitionsService = inject(AssetDefinitionsService);
this.translateService = inject(TranslateService);
}
/**
* Filters added properties to only include those compatible with the given asset.
* Currently only checks compatibility for computed properties.
* @param allAddedProperties All properties that have been added by the user
* @param asset The current asset context
* @returns Promise resolving to properties compatible with the asset
*/
async filterCompatibleProperties(allAddedProperties, asset) {
if (!asset || !allAddedProperties.length) {
return allAddedProperties;
}
let availableComputedPropertyNames = null;
const computedProperties = allAddedProperties.filter(prop => prop.computed);
if (computedProperties.length > 0) {
const availableComputedProperties = await this.computedPropertiesService.getByContext(asset);
availableComputedPropertyNames = new Set(availableComputedProperties.map(({ prop }) => prop.name));
}
const compatibleProperties = [];
for (const property of allAddedProperties) {
if (await this.isPropertyAvailable(property, availableComputedPropertyNames)) {
compatibleProperties.push(property);
}
}
return compatibleProperties;
}
/**
* Retrieves properties for an asset.
* First, it tries to get properties from definitions API (if enabled),
* then falls back to asset library.
* @param asset The asset for which to retrieve custom properties.
* @returns A promise resolving to the list of custom properties.
*/
async getAssetProperties(asset) {
if (!asset?.type) {
return [];
}
const isModelApiEnabled = await firstValueFrom(this.featureCacheService.getFeatureState(this.MODEL_API_FEATURE_KEY));
if (isModelApiEnabled) {
try {
const assetDefinition = await this.assetDefinitionsService.getByIdentifier(asset.type);
if (assetDefinition) {
return this.categorizePropertiesFromDefinition(assetDefinition);
}
}
catch {
// do nothing- fallback to asset library
}
}
const assetType = await firstValueFrom(this.assetTypesRealtimeService.getAssetTypeByName$(asset.type));
if (assetType) {
const { data } = await this.inventoryService.childAdditionsList(assetType, {
pageSize: 2000,
query: "$filter=(has('c8y_IsAssetProperty'))"
});
return this.categorizeCustomProperties(data);
}
else {
return [];
}
}
/**
* Retrieves the initial set of properties for an asset, based on its type.
* @param asset The asset for which to retrieve properties.
* @returns A promise resolving to the list of initial properties.
*/
async getInitialProperties(asset) {
if (!asset) {
return [];
}
else if (this.groupService.isDevice(asset)) {
return await this.getDeviceProperties(asset);
}
else if (this.groupService.isGroup(asset) && !this.groupService.isAsset(asset)) {
return await this.getGroupProperties(asset);
}
else if (this.groupService.isAsset(asset)) {
return await this.getAssetProperties(asset);
}
return [];
}
/**
* Retrieves properties for a device asset.
* @param asset The device asset for which to retrieve properties.
* @returns A promise resolving to the list of device properties.
*/
async getDeviceProperties(asset) {
return this.getManagedObjectProperties(asset);
}
/**
* Retrieves properties for a group asset.
* @param asset The group asset for which to retrieve properties.
* @returns A promise resolving to the list of group properties.
*/
async getGroupProperties(asset) {
return this.getManagedObjectProperties(asset);
}
/**
* Categorizes custom properties into simple and complex types.
* @param properties The custom properties to categorize.
* @returns The categorized custom properties.
*/
categorizeCustomProperties(properties) {
const { simple, complex } = properties.reduce((acc, property) => {
const schema = property.c8y_JsonSchema.properties[property.name];
if (schema.type === 'object') {
acc.complex.push(property);
}
else {
acc.simple.push(property);
}
return acc;
}, { simple: [], complex: [] });
return [...simple, ...complex];
}
/**
* Categorizes and flattens hierarchical properties into simple and complex types.
* @param properties The hierarchical properties to categorize and flatten.
* @returns The categorized and flattened properties.
*/
categorizeAndFlattenHierarchicalProperties(properties) {
const sortedProperties = [...properties].sort((a, b) => {
const aLabel = (a.label || a.name || '').toLowerCase();
const bLabel = (b.label || b.name || '').toLowerCase();
return aLabel.localeCompare(bLabel);
});
const result = sortedProperties.reduce((acc, property) => {
property.active = false;
if (property.computed) {
acc.computed.push(property);
}
else if (this.isComplexProperty(property)) {
acc.complex.push(property);
this.addNestedProperties(property, acc.complex, true);
}
else {
acc.simple.push(property);
}
return acc;
}, { computed: [], simple: [], complex: [] });
return result;
}
/**
* Checks if a property is complex (i.e., has nested properties).
* @param property The property to check.
* @returns True if the property is complex, false otherwise.
*/
isComplexProperty(property) {
return (property.c8y_JsonSchema?.properties[property.name]?.type === 'object' ||
property.properties !== undefined);
}
/**
* Checks if property is available based on provided available computed property names.
* @param property The property to check.
* @param availableComputedPropertyNames Set of available computed property names.
* @returns True if the property is available, false otherwise.
*/
async isPropertyAvailable(property, availableComputedPropertyNames) {
if (property.computed) {
if (!availableComputedPropertyNames) {
return false;
}
return availableComputedPropertyNames.has(property.name);
}
return true;
}
/**
* Checks if two properties match for selection purposes.
* @param property1 First property to compare.
* @param property2 Second property to compare.
* @param omitProperties Optional list of property keys to omit from comparison (e.g., ['instanceId','configuredAssetId']).
* @returns True if properties match.
*/
propertiesMatch(property1, property2, omitProperties = []) {
if (property1.name === property2.name) {
const propertiesToOmit = ['active', ...omitProperties];
const areEqual = isEqual(omit(property1, propertiesToOmit), omit(property2, propertiesToOmit));
return areEqual;
}
return false;
}
/**
* Retrieves custom properties from the properties library, optionally filtered by search text.
* @param searchText Optional search text to filter properties.
* @returns A promise resolving to the list of properties and paging information.
*/
async getPropertiesFromPropertiesLibrary(searchText) {
const propertiesFromLibrary = [];
const { data: propertiesMOs, paging } = await this.requestPropertiesFromPropertiesLibrary(searchText);
propertiesMOs.forEach(prop => {
const name = Object.keys(prop.c8y_JsonSchema.properties)[0];
propertiesFromLibrary.push({
c8y_JsonSchema: prop.c8y_JsonSchema,
label: prop.name,
name,
type: prop.c8y_JsonSchema.properties[name].type
});
});
return { propertiesFromLibrary, paging };
}
/**
* Filters properties with hierarchical search logic:
* - Simple properties: match label or name
* - Child properties: match child label/name AND if matches- match also parent
* - Complex properties: match parent label/name AND if matches- all children
*
* @example
* // Search "or" → matches children "Major", "Minor" → includes parent + matching children
* filterPropertiesWithHierarchy([
* { name: 'c8y_ActiveAlarmsStatus', label: 'Active alarms status', type: 'object', ... },
* { name: 'major', label: 'Major', keyPath: ['c8y_ActiveAlarmsStatus', 'major'], ... },
* { name: 'minor', label: 'Minor', keyPath: ['c8y_ActiveAlarmsStatus', 'minor'], ... },
* { name: 'critical', label: 'Critical', keyPath: ['c8y_ActiveAlarmsStatus', 'critical'], ... }
* ], 'or')
* // Returns: [
* { name: 'c8y_ActiveAlarmsStatus', label: 'Active alarms status', type: 'object', ... },
* { name: 'major', label: 'Major', keyPath: ['c8y_ActiveAlarmsStatus', 'major'], ... },
* { name: 'minor', label: 'Minor', keyPath: ['c8y_ActiveAlarmsStatus', 'minor'], ... }
* ]
*
* @example
* // Search "address" → matches parent → includes parent + all children
* filterPropertiesWithHierarchy([
* { name: 'c8y_Address', label: 'Address', type: 'object', ... },
* { name: 'street', label: 'Street', keyPath: ['c8y_Address', 'street'], ... },
* { name: 'city', label: 'City', keyPath: ['c8y_Address', 'city'], ... }
* ], 'address')
* // Returns: [
* { name: 'c8y_Address', label: 'Address', type: 'object', ... },
* { name: 'street', label: 'Street', keyPath: ['c8y_Address', 'street'], ... },
* { name: 'city', label: 'City', keyPath: ['c8y_Address', 'city'], ... }
* ]
*
* @param flattenedProperties All flattened properties (simple and complex)
* @param searchTerm Search term (case-insensitive, already lowercased)
* @returns Filtered properties matching the search term
*/
filterPropertiesWithHierarchy(flattenedProperties, searchTerm) {
if (!searchTerm) {
return flattenedProperties;
}
const lowercasedSearchTerm = searchTerm.toLowerCase();
// Extract complex properties for parent lookups
const complexProperties = flattenedProperties.filter(prop => this.isComplexProperty(prop));
const parentMap = new Map(complexProperties.map(p => [p.name, p]));
return flattenedProperties
.map(prop => {
if (!this.isComplexProperty(prop) && !prop.keyPath) {
return this.matchesSimpleProperty(prop, lowercasedSearchTerm);
}
else if (!this.isComplexProperty(prop) && prop.keyPath) {
return this.matchesChildProperty(prop, lowercasedSearchTerm, parentMap);
}
else {
return this.matchesComplexProperty(prop, lowercasedSearchTerm);
}
})
.filter(prop => prop !== null);
}
/**
* Checks if a simple property (which is not a child of a complex property) matches the search term.
* @param prop The property to check.
* @param searchTerm The search term (already lowercased).
* @returns The property if it matches, otherwise null.
*/
matchesSimpleProperty(prop, searchTerm) {
const matches = prop.name.toLowerCase().includes(searchTerm) ||
prop.label.toLowerCase().includes(searchTerm) ||
this.translateService.instant(prop.label).toLowerCase().includes(searchTerm);
return matches ? prop : null;
}
/**
* Checks if a simple property (which is a child of a complex property) matches the search term and if not- check if parent complex property matches.
* @param prop The property to check.
* @param searchTerm The search term (already lowercased).
* @param parentMap A map of parent complex properties.
* @returns The property if it matches, otherwise null.
*/
matchesChildProperty(prop, searchTerm, parentMap) {
// Check if child property matches
const childMatches = prop.name.toLowerCase().includes(searchTerm) ||
prop.label.toLowerCase().includes(searchTerm) ||
this.translateService.instant(prop.label).toLowerCase().includes(searchTerm);
if (childMatches) {
return prop;
}
// Check if parent complex property matches
const parentName = prop.keyPath[0];
const parentProp = parentMap.get(parentName);
if (parentProp) {
const parentMatches = parentProp.name.toLowerCase().includes(searchTerm) ||
parentProp.label.toLowerCase().includes(searchTerm) ||
this.translateService.instant(parentProp.label).toLowerCase().includes(searchTerm);
return parentMatches ? prop : null;
}
return null;
}
/**
* Checks if a complex property matches the search term or if any of its child properties match the search term.
* @param prop The complex property to check.
* @param searchTerm The search term (already lowercased).
* @returns The property if it matches, otherwise null.
*/
matchesComplexProperty(prop, searchTerm) {
// Complex property
const parentMatches = prop.name.toLowerCase().includes(searchTerm) ||
prop.label.toLowerCase().includes(searchTerm) ||
this.translateService.instant(prop.label).toLowerCase().includes(searchTerm);
if (parentMatches) {
return prop;
}
// If parent doesn't match, check children properties- if any of the children match, then we include the parent complex property
// Get nested properties from the schema
const nestedPropertiesObj = prop.c8y_JsonSchema?.properties[prop.name]?.properties;
if (nestedPropertiesObj) {
// Check if any nested property matches the search term
const matchingNestedProperty = Object.keys(nestedPropertiesObj).find(key => {
const nestedProp = nestedPropertiesObj[key];
return (key.toLowerCase().includes(searchTerm) ||
(nestedProp.title || '').toLowerCase().includes(searchTerm) ||
(nestedProp.title &&
this.translateService
.instant(nestedProp.title || '')
.toLowerCase()
.includes(searchTerm)));
});
if (matchingNestedProperty) {
return prop;
}
else {
return null;
}
}
else {
return null;
}
}
async requestPropertiesFromPropertiesLibrary(searchText) {
let queryFilter = '(type eq c8y_JsonSchema) and (appliesTo.MANAGED_OBJECTS eq true)';
if (searchText?.trim()) {
const escapedSearchText = searchText.replace(/'/g, "''");
queryFilter += ` and (name eq '*${escapedSearchText}*')`;
}
const query = `$filter=(${queryFilter})`;
const filter = {
pageSize: 20,
revert: true,
query,
withTotalPages: true,
withTotalElements: true
};
try {
const results = await this.inventoryService.list(filter);
return results;
}
catch (error) {
this.alert.addServerFailure(error);
return null;
}
}
addNestedProperties(parentProperty, complexProperties, sortChildren = false) {
const schema = parentProperty.c8y_JsonSchema?.properties[parentProperty.name];
if (!schema?.properties) {
return;
}
this.flattenProperties(schema, complexProperties, parentProperty.name, [], sortChildren);
}
flattenProperties(schema, result, parentName, parentPath = [], sortChildren = false) {
const properties = schema.properties?.[parentName]?.properties || schema.properties;
let entries = Object.entries(properties);
if (sortChildren) {
entries = entries.sort((a, b) => {
const aLabel = (a[1].title || a[1].label || a[1].name || a[0] || '').toLowerCase();
const bLabel = (b[1].title || b[1].label || b[1].name || b[0] || '').toLowerCase();
return aLabel.localeCompare(bLabel);
});
}
entries.forEach(([key, property]) => {
const path = parentPath.includes(parentName) ? parentPath : [...parentPath, parentName];
result.push({
...property,
name: key,
label: property.label || property.title || key,
keyPath: [...path, key]
});
if (property.properties) {
this.flattenProperties({ properties: { [key]: property } }, result, key, [...path, key], sortChildren);
}
});
}
getManagedObjectProperties(asset) {
return this.extractFragments(asset);
}
extractFragments(object) {
const properties = [];
for (const [key, value] of Object.entries(object)) {
if (this.shouldSkipFragment(key) ||
isArray(value) ||
(isObjectLike(value) && isEmpty(value))) {
continue;
}
const newProp = {
label: key,
name: key,
type: isObjectLike(value) ? 'object' : 'string',
isEditable: true,
c8y_JsonSchema: {
properties: {
[key]: {
key: key,
type: isObjectLike(value) ? 'object' : 'string',
label: key,
properties: {}
}
}
}
};
if (isObjectLike(value)) {
this.addPropertyItem(newProp.c8y_JsonSchema.properties[key].properties, value);
}
properties.push(newProp);
}
return properties;
}
shouldSkipFragment(key) {
return !!find(this.FRAGMENTS_TO_OMIT, (fragmentToOmit) => {
if (fragmentToOmit instanceof RegExp) {
return fragmentToOmit.test(key);
}
return fragmentToOmit === key;
});
}
addPropertyItem(properties, object) {
if (!properties) {
return;
}
forOwn(object, (value, key) => {
properties[key] = { title: key, type: value ? typeof value : 'string' };
if (isObjectLike(value)) {
properties[key].type = 'object';
this.addPropertyItem(properties[key]['properties'], value);
}
});
}
/**
* Converts asset definition from model API into AssetPropertyType array.
* @param assetDefinition The asset definition to convert.
* @returns The converted custom properties.
*/
categorizePropertiesFromDefinition(assetDefinition) {
if (!assetDefinition?.jsonSchema?.properties) {
return [];
}
const { properties, definitions } = assetDefinition.jsonSchema;
const propertyObjects = [];
for (const [propertyName, propertySchema] of Object.entries(properties)) {
let resolvedSchema = propertySchema;
// Resolve $ref if present
if (propertySchema.$ref && definitions) {
const refPath = propertySchema.$ref.replace('#/definitions/', '');
resolvedSchema = definitions[refPath] || propertySchema;
}
if (!resolvedSchema || !resolvedSchema.type) {
continue;
}
const propertyObject = {
name: propertyName,
label: resolvedSchema.title || propertyName,
c8y_JsonSchema: {
properties: {
[propertyName]: resolvedSchema
}
},
type: resolvedSchema.type
};
propertyObjects.push(propertyObject);
}
return this.categorizeCustomProperties(propertyObjects);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetPropertiesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetPropertiesService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetPropertiesService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class FlatTreeDataSource extends DataSource {
constructor() {
super();
this._dataChange = new BehaviorSubject([]);
}
get data() {
return this._dataChange.value;
}
set data(value) {
this._dataChange.next(value);
}
connect() {
return this._dataChange.asObservable();
}
disconnect() {
// No need to unsubscribe from the _dataChange subject since it's a BehaviorSubject
}
}
class AssetPropertyActionDirective {
constructor(template, elementRef, viewContainer) {
this.template = template;
this.elementRef = elementRef;
this.viewContainer = viewContainer;
}
static { this.ɵfac =