@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 283 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-asset-properties.mjs","sources":["../../asset-properties/asset-properties.model.ts","../../asset-properties/computed-properties.service.ts","../../asset-properties/asset-properties.service.ts","../../asset-properties/asset-property-list/tree-data-source.ts","../../asset-properties/asset-property-list/asset-property-action.directive.ts","../../asset-properties/asset-property-list/asset-property-value.pipe.ts","../../asset-properties/asset-property-list/asset-property-icon.pipe.ts","../../asset-properties/asset-property-list/asset-property-icon-tooltip.pipe.ts","../../asset-properties/computed-properties-config/computed-properties-config.component.ts","../../asset-properties/computed-properties-config/computed-properties-config.component.html","../../asset-properties/asset-property-list/asset-property-collapse-button-title.pipe.ts","../../asset-properties/asset-property-list/asset-property-drag-drop.service.ts","../../asset-properties/asset-property-list/asset-property-list.component.ts","../../asset-properties/asset-property-list/asset-property-list.component.html","../../asset-properties/asset-property-tabs/asset-property-tabs.component.ts","../../asset-properties/asset-property-tabs/asset-property-tabs.component.html","../../asset-properties/custom-properties-drawer/custom-properties-drawer.component.ts","../../asset-properties/custom-properties-drawer/custom-properties-drawer.component.html","../../asset-properties/custom-properties-drawer.service.ts","../../asset-properties/asset-property-list/asset-property-label.pipe.ts","../../asset-properties/asset-property-selector/asset-property-selector.component.ts","../../asset-properties/asset-property-selector/asset-property-selector.component.html","../../asset-properties/asset-property-selector-drawer/asset-property-selector-drawer.component.ts","../../asset-properties/asset-property-selector-drawer/asset-property-selector-drawer.component.html","../../asset-properties/c8y-ngx-components-asset-properties.ts"],"sourcesContent":["import { Injector, Type } from '@angular/core';\nimport { IAlarm, IEvent, IIdentified, IManagedObject } from '@c8y/client';\nimport { C8yJsonSchema, C8yPropertyType } from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { Observable } from 'rxjs';\n\ninterface ComputedPropertyContextMap {\n device: { type: 'device'; value: IManagedObject };\n group: { type: 'group'; value: IManagedObject };\n asset: { type: 'asset'; value: IManagedObject };\n alarm: { type: 'alarm'; value: IAlarm };\n event: { type: 'event'; value: IEvent };\n}\n\nexport type ValueCallbackMetadata = {\n /**\n * Mode of operation:\n * - 'singleValue': Get count only once (no real-time updates)\n * - 'realtime': Enable real-time updates (default)\n */\n mode?: 'singleValue' | 'realtime';\n /**\n * Observable to control real-time updates when mode is 'realtime'.\n * - true: Enable real-time updates (fetches current count from server and continues)\n * - false: Disable real-time updates (preserves current count, no server call)\n * If not provided, real-time runs continuously.\n */\n realtimeControl$?: Observable<boolean>;\n};\n\nexport type ComputedPropertyContextType = keyof ComputedPropertyContextMap;\nexport type ComputedPropertyContextValue =\n ComputedPropertyContextMap[keyof ComputedPropertyContextMap]['value'];\n\ntype ComputedPropertyDefinitionBase<\n TContext extends readonly ComputedPropertyContextType[] = ComputedPropertyContextType[],\n TConfig = any\n> = {\n name: string;\n prop: BaseProperty;\n contextType: TContext;\n value: ({\n config,\n context,\n metadata\n }: {\n config?: TConfig;\n context?: ComputedPropertyContextMap[TContext[number]]['value'];\n metadata?: ValueCallbackMetadata;\n }) => any | Observable<any> | Promise<any>;\n injector?: Injector;\n};\n\nexport type ComputedPropertyDefinition<\n TContext extends readonly ComputedPropertyContextType[] = ComputedPropertyContextType[],\n TConfig = any\n> = ComputedPropertyDefinitionBase<TContext, TConfig> &\n (\n | {\n configComponent?: never;\n loadConfigComponent?: never;\n }\n | {\n configComponent: Type<any>;\n loadConfigComponent?: never;\n }\n | {\n configComponent?: never;\n loadConfigComponent: () => Promise<Type<any>>;\n }\n );\n\nexport type ComputedPropertyComponent = {\n /**\n * The configuration which is shared between configuration component and display component.\n * Should be serializable to allow saving it to the API.\n */\n config: unknown;\n /**\n * The asset, device, group, alarm or event for which the property is displayed.\n */\n asset?: IManagedObject;\n};\n\nexport type AssetPropertyListConfig = {\n /**\n * List of properties that should be selected by default when the asset property list is loaded.\n */\n selectedProperties?: AssetPropertyType[];\n /**\n * Flag to enable or disable the filter functionality in the asset property list.\n * When enabled, a search input will be displayed to filter properties by their names.\n */\n filterable?: boolean;\n /**\n * The mode of the asset property list describing how the properties are selected.\n * - `single` - only one property can be selected at a time\n * - `multi` - multiple properties can be selected at a time\n * - `plus` - multiple properties can be selected using add/remove buttons\n * - `none` - no properties can be selected\n */\n selectMode?: 'single' | 'multi' | 'plus' | 'none';\n /**\n * The mode of the asset property list describing how the properties are expanded.\n * - `expandedByDefault` - all properties are expanded by default and collapse button is shown\n * - `collapsedByDefault` - all properties are collapsed by default and expand button is shown\n * - `nonCollapsible` - all properties are expanded all the time and collapse button is hidden\n */\n expansionMode?: 'expandedByDefault' | 'collapsedByDefault' | 'nonCollapsible';\n /**\n * Flag to show or hide the header of the asset property list.\n * Header includes select all checkbox (if applicable) and the title of the list columns.\n */\n showHeader?: boolean;\n /**\n * Flag to show or hide the value of the asset property.\n */\n showValue?: boolean;\n /**\n * Flag to show or hide the key of the asset property.\n * If set to true, the key of the property will be displayed alongside its value.\n */\n showKey?: boolean;\n /**\n * Defines what should be displayed when asset is not selected and there are no properties to show.\n * - `empty` - empty state with a message\n * - `default-properties` - default properties are shown (like ID, name, type, etc.)\n */\n emptyStateContent?: 'empty' | 'default-properties';\n /**\n * Flag to allow adding custom properties to the asset property list with additional dialog.\n */\n allowAddingCustomProperties?: boolean;\n /**\n * Asset property list allows to provide custom properties as an input.\n * This flag defines how the input properties are handled.\n * - `merge` - input properties are merged with properties from asset list\n * - `override` - input properties override properties from asset list, meaning that only input properties are shown\n */\n inputPropertiesHandle?: 'merge' | 'override';\n /**\n * Enable or disable drag and drop functionality for reordering properties.\n */\n allowDragAndDrop?: boolean;\n /**\n * Tabs to hide in the custom properties drawer.\n */\n hiddenTabs?: HiddenTabs;\n /**\n * Flag to add context asset's ID and name to output properties.\n */\n addAssetToProperty?: boolean;\n};\n\nexport const defaultAssetPropertyListConfig: AssetPropertyListConfig = {\n filterable: true,\n selectMode: 'none',\n expansionMode: 'expandedByDefault',\n showHeader: true,\n showValue: true,\n showKey: true,\n emptyStateContent: 'empty',\n allowAddingCustomProperties: false,\n inputPropertiesHandle: 'merge',\n allowDragAndDrop: false\n};\n\nexport const defaultAssetProperties: Array<BaseProperty> = [\n {\n c8y_JsonSchema: { properties: { name: { type: 'string', label: 'Name' } } },\n name: 'name',\n label: gettext('Name'),\n type: 'string',\n active: true,\n isEditable: true,\n isStandardProperty: true\n },\n {\n c8y_JsonSchema: { properties: { id: { type: 'string', label: 'ID' } } },\n name: 'id',\n label: gettext('ID'),\n type: 'string',\n active: true,\n isEditable: false,\n isStandardProperty: true\n },\n {\n c8y_JsonSchema: {\n properties: { type: { type: 'string', label: 'Type' } }\n },\n name: 'type',\n label: gettext('Type'),\n type: 'string',\n active: true,\n isEditable: false,\n isStandardProperty: true\n },\n {\n c8y_JsonSchema: {\n properties: { owner: { type: 'string', label: 'Owner' } }\n },\n name: 'owner',\n label: gettext('Owner'),\n type: 'string',\n isEditable: false,\n isStandardProperty: true\n },\n {\n c8y_JsonSchema: {\n properties: { lastUpdated: { type: 'string', label: 'Last updated' } }\n },\n name: 'lastUpdated',\n label: gettext('Last updated'),\n type: 'string',\n isEditable: false,\n isStandardProperty: true\n },\n {\n label: gettext('Active alarms status'),\n type: 'object',\n isEditable: false,\n isStandardProperty: true,\n name: 'c8y_ActiveAlarmsStatus',\n c8y_JsonSchema: {\n properties: {\n c8y_ActiveAlarmsStatus: {\n key: 'c8y_ActiveAlarmsStatus',\n type: 'object',\n label: 'Active alarms status',\n properties: {\n critical: {\n title: 'Critical',\n type: 'number'\n },\n major: {\n title: 'Major',\n type: 'number'\n },\n minor: {\n title: 'Minor',\n type: 'number'\n },\n warning: {\n title: 'Warning',\n type: 'number'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Address'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_Address',\n c8y_JsonSchema: {\n properties: {\n c8y_Address: {\n key: 'c8y_Address',\n type: 'object',\n label: 'Address',\n properties: {\n street: {\n title: 'Street',\n type: 'string'\n },\n city: {\n title: 'City',\n type: 'string'\n },\n cityCode: {\n title: 'City code',\n type: 'string'\n },\n territory: {\n title: 'Territory',\n type: 'string'\n },\n region: {\n title: 'Region',\n type: 'string'\n },\n country: {\n title: 'Country',\n type: 'string'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Agent'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_Agent',\n c8y_JsonSchema: {\n properties: {\n c8y_Agent: {\n key: 'c8y_Agent',\n type: 'object',\n label: 'Agent',\n properties: {\n name: {\n title: 'Name',\n type: 'string'\n },\n version: {\n title: 'Version',\n type: 'string'\n },\n url: {\n title: 'URL',\n type: 'string'\n },\n maintainer: {\n title: 'Maintainer',\n type: 'string'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Availability'),\n type: 'object',\n isEditable: false,\n isStandardProperty: true,\n name: 'c8y_Availability',\n c8y_JsonSchema: {\n properties: {\n c8y_Availability: {\n key: 'c8y_Availability',\n type: 'object',\n label: 'Availability',\n properties: {\n status: {\n title: 'Status',\n type: 'string'\n },\n lastMessage: {\n title: 'Last message',\n type: 'string',\n printFormat: 'datetime'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Connection'),\n type: 'object',\n isEditable: false,\n isStandardProperty: true,\n name: 'c8y_Connection',\n c8y_JsonSchema: {\n properties: {\n c8y_Connection: {\n key: 'c8y_Connection',\n type: 'object',\n label: 'Connection',\n properties: {\n status: {\n title: 'Status',\n type: 'string'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Communication mode'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_CommunicationMode',\n c8y_JsonSchema: {\n properties: {\n c8y_CommunicationMode: {\n key: 'c8y_CommunicationMode',\n type: 'object',\n label: 'Communication mode',\n properties: {\n mode: {\n title: 'Mode',\n type: 'string'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Firmware'),\n type: 'object',\n isEditable: false,\n isStandardProperty: true,\n name: 'c8y_Firmware',\n c8y_JsonSchema: {\n properties: {\n c8y_Firmware: {\n key: 'c8y_Firmware',\n type: 'object',\n label: 'Firmware',\n properties: {\n moduleVersion: {\n title: 'Module version',\n type: 'string'\n },\n name: {\n title: 'Name',\n type: 'string'\n },\n version: {\n title: 'Version',\n type: 'string'\n },\n url: {\n title: 'URL',\n type: ['string', 'null']\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Hardware'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_Hardware',\n c8y_JsonSchema: {\n properties: {\n c8y_Hardware: {\n key: 'c8y_Hardware',\n type: 'object',\n label: 'Hardware',\n properties: {\n model: {\n title: 'Model',\n type: 'string'\n },\n serialNumber: {\n title: 'Serial number',\n type: 'string'\n },\n revision: {\n title: 'Revision',\n type: 'string'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('LPWAN device'),\n type: 'object',\n isEditable: false,\n isStandardProperty: true,\n name: 'c8y_LpwanDevice',\n c8y_JsonSchema: {\n properties: {\n c8y_LpwanDevice: {\n key: 'c8y_LpwanDevice',\n type: 'object',\n label: 'LPWAN device',\n properties: {\n provisioned: {\n title: 'Provisioned',\n type: 'boolean'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Mobile'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_Mobile',\n c8y_JsonSchema: {\n properties: {\n c8y_Mobile: {\n key: 'c8y_Mobile',\n type: 'object',\n label: 'Mobile',\n properties: {\n cellId: {\n title: 'Cell ID',\n type: ['string', 'null']\n },\n connType: {\n title: 'Connection type',\n type: 'string',\n readOnly: true\n },\n currentOperator: {\n title: 'Current operator',\n type: 'string',\n readOnly: true\n },\n currentBand: {\n title: 'Current band',\n type: 'string',\n readOnly: true\n },\n ecn0: {\n title: 'ECN0',\n type: 'string',\n readOnly: true\n },\n iccid: {\n title: 'ICCID',\n type: ['string', 'null']\n },\n imei: {\n title: 'IMEI',\n type: ['string', 'null']\n },\n imsi: {\n title: 'IMSI',\n type: ['string', 'null']\n },\n lac: {\n title: 'LAC',\n type: ['string', 'null']\n },\n mcc: {\n title: 'MCC',\n type: ['string', 'null']\n },\n mnc: {\n title: 'MNC',\n type: ['string', 'null']\n },\n msisdn: {\n title: 'MSISDN',\n type: 'string'\n },\n rcsp: {\n title: 'RCSP',\n type: 'string',\n readOnly: true\n },\n rscp: {\n title: 'RSCP',\n type: 'string',\n readOnly: true\n },\n rsrp: {\n title: 'RSRP',\n type: 'string',\n readOnly: true\n },\n rsrq: {\n title: 'RSRQ',\n type: 'string',\n readOnly: true\n },\n rssi: {\n title: 'RSSI',\n type: 'string',\n readOnly: true\n }\n }\n }\n }\n }\n },\n {\n name: 'c8y_Notes',\n label: 'Notes',\n type: 'string',\n isEditable: true,\n isStandardProperty: true,\n c8y_JsonSchema: {\n properties: {\n c8y_Notes: {\n type: 'string',\n label: 'Notes',\n 'x-schema-form': {\n type: 'textarea'\n }\n }\n }\n }\n },\n {\n label: gettext('Position'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_Position',\n c8y_JsonSchema: {\n properties: {\n c8y_Position: {\n key: 'c8y_Position',\n type: 'object',\n label: 'Position',\n properties: {\n lat: {\n title: 'Latitude',\n type: 'number'\n },\n lng: {\n title: 'Longitude',\n type: 'number'\n },\n alt: {\n title: 'Altitude',\n type: 'number'\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Required availability'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_RequiredAvailability',\n c8y_JsonSchema: {\n properties: {\n c8y_RequiredAvailability: {\n key: 'c8y_RequiredAvailability',\n type: 'object',\n label: 'Required availability',\n properties: {\n responseInterval: {\n title: 'Response interval',\n description:\n 'Takes a value between -32768 and 32767 minutes (a negative value indicates that the device is under maintenance).',\n type: 'integer',\n minimum: -32768,\n maximum: 32767\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Software'),\n type: 'object',\n isEditable: false,\n isStandardProperty: true,\n name: 'c8y_Software',\n c8y_JsonSchema: {\n properties: {\n c8y_Software: {\n key: 'c8y_Software',\n type: 'object',\n label: 'Software',\n properties: {\n name: {\n title: 'Name',\n type: 'string'\n },\n version: {\n title: 'Version',\n type: 'string'\n },\n url: {\n title: 'URL',\n type: ['string', 'null']\n }\n }\n }\n }\n }\n },\n {\n label: gettext('Network'),\n type: 'object',\n isEditable: true,\n isStandardProperty: true,\n name: 'c8y_Network',\n c8y_JsonSchema: {\n properties: {\n c8y_Network: {\n key: 'c8y_Network',\n type: 'object',\n label: 'Network',\n properties: {\n c8y_DHCP: {\n title: 'DHCP',\n type: 'object',\n printFormat: 'hidden',\n name: 'c8y_DHCP',\n properties: {\n addressRange: {\n title: 'Address range',\n type: 'object',\n name: 'addressRange',\n printFormat: 'hidden',\n properties: {\n start: {\n title: 'Start',\n type: 'string'\n },\n end: {\n title: 'End',\n type: 'string'\n }\n }\n },\n dns1: {\n title: 'DNS 1',\n type: 'string'\n },\n dns2: {\n title: 'DNS 2',\n type: 'string'\n },\n enabled: {\n title: 'Enabled',\n type: 'integer'\n }\n }\n },\n c8y_LAN: {\n title: 'LAN',\n type: 'object',\n name: 'c8y_LAN',\n printFormat: 'hidden',\n properties: {\n enabled: {\n title: 'Enabled',\n type: 'integer'\n },\n ip: {\n title: 'IP',\n type: 'string'\n },\n mac: {\n title: 'MAC',\n type: 'string'\n },\n name: {\n title: 'Name',\n type: 'string'\n },\n netmask: {\n title: 'Netmask',\n type: 'string'\n }\n }\n },\n c8y_WAN: {\n title: 'WAN',\n type: 'object',\n name: 'c8y_WAN',\n printFormat: 'hidden',\n properties: {\n apn: {\n title: 'APN',\n type: 'string'\n },\n authType: {\n title: 'Auth type',\n type: 'string'\n },\n ip: {\n title: 'IP',\n type: 'string'\n },\n password: {\n title: 'Password',\n type: 'string'\n },\n simStatus: {\n title: 'SIM status',\n type: 'string'\n },\n username: {\n title: 'Username',\n type: 'string'\n }\n }\n }\n }\n }\n }\n }\n }\n];\n\nexport const RESULT_TYPES = {\n VALUE: { name: 'VALUE', value: 1, label: gettext('Only value') },\n VALUE_UNIT: { name: 'VALUE_UNIT', value: 2, label: gettext('Value and unit') },\n VALUE_UNIT_TIME: { name: 'VALUE_UNIT_TIME', value: 3, label: gettext('Value, unit and time') }\n};\n\nexport interface BaseProperty {\n name: string;\n label: string;\n type: C8yPropertyType;\n isEditable?: boolean;\n temporary?: boolean;\n isStandardProperty?: boolean;\n c8y_JsonSchema?: C8yJsonSchema;\n active?: boolean;\n computed?: boolean;\n instanceId?: string;\n asset?: IIdentified;\n [key: string]: any;\n}\n\nexport interface NestedPropertyFields extends BaseProperty {\n title?: string;\n properties?: object;\n keyPath: string[];\n}\n\nexport type AssetPropertyType = BaseProperty | NestedPropertyFields;\n\nexport type HiddenTabs = { regular?: boolean; computed?: boolean; asset?: boolean };\n\nexport type AssetPropertyTab = keyof Required<HiddenTabs>;\n","import { inject, Injectable, InjectionToken, Injector } from '@angular/core';\nimport {\n ExtensionPointForPlugins,\n GenericHookOptions,\n GenericHookType,\n hookGeneric,\n PluginsResolveService,\n getInjectedHooks,\n stateToFactory,\n fromTriggerOnce,\n GroupService\n} from '@c8y/ngx-components';\nimport {\n ComputedPropertyContextType,\n ComputedPropertyContextValue,\n ComputedPropertyDefinition\n} from './asset-properties.model';\nimport { Router } from '@angular/router';\nimport {\n distinctUntilChanged,\n filter,\n firstValueFrom,\n map,\n mergeMap,\n Observable,\n shareReplay,\n take\n} from 'rxjs';\nimport { IAlarm, IEvent, IManagedObject } from '@c8y/client';\n\nexport const HOOK_COMPUTED_PROPERTY = new InjectionToken<ComputedPropertyDefinition[]>(\n 'HOOK_COMPUTED_PROPERTY'\n);\n\nexport function hookComputedProperty(\n property: GenericHookType<ComputedPropertyDefinition>,\n options?: Partial<GenericHookOptions>\n) {\n return hookGeneric<ComputedPropertyDefinition>(property, HOOK_COMPUTED_PROPERTY, options);\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class ComputedPropertiesService extends ExtensionPointForPlugins<ComputedPropertyDefinition> {\n private groupService = inject(GroupService);\n\n constructor(\n rootInjector: Injector,\n private router: Router,\n plugins: PluginsResolveService\n ) {\n super(rootInjector, plugins);\n this.items$ = this.setupItemsObservable();\n }\n\n /**\n * Returns the current state.\n * @readonly\n * @returns The current set of computed properties.\n */\n get state(): Set<ComputedPropertyDefinition> {\n return this.state$.value;\n }\n\n add(propertyDef: ComputedPropertyDefinition) {\n this.state.add(propertyDef);\n this.emitNewState();\n }\n\n getByName(name: string): Promise<ComputedPropertyDefinition> {\n if (!name) {\n return Promise.resolve(undefined);\n }\n return firstValueFrom(\n this.items$.pipe(\n mergeMap((propertyDefs: ComputedPropertyDefinition[]) => propertyDefs),\n filter((propertyDef: ComputedPropertyDefinition) => propertyDef.name === name),\n take(1)\n )\n );\n }\n\n getByContext(asset: ComputedPropertyContextValue): Promise<ComputedPropertyDefinition[]> {\n if (!asset) {\n return Promise.resolve([]);\n }\n const computedPropertyContextType: ComputedPropertyContextType = this.getTypeOfContext(asset);\n\n return firstValueFrom(\n this.items$.pipe(\n map((propertyDefs: ComputedPropertyDefinition[]) =>\n propertyDefs.filter((propertyDef: ComputedPropertyDefinition) => {\n if (!propertyDef.contextType) {\n return true;\n }\n return propertyDef.contextType.includes(computedPropertyContextType);\n })\n )\n )\n );\n }\n\n private getTypeOfContext(context: ComputedPropertyContextValue): ComputedPropertyContextType {\n const isEvent = (item: IAlarm | IEvent): item is IEvent => !('severity' in item);\n const isAlarm = (item: IAlarm | IEvent): item is IAlarm => 'severity' in item;\n\n if (this.groupService.isDevice(context as IManagedObject)) {\n return 'device';\n } else if (\n this.groupService.isGroup(context as IManagedObject) &&\n !this.groupService.isAsset(context as IManagedObject)\n ) {\n return 'group';\n } else if (this.groupService.isAsset(context as IManagedObject)) {\n return 'asset';\n } else if (isAlarm(context as IAlarm | IEvent)) {\n return 'alarm';\n } else if (isEvent(context as IAlarm | IEvent)) {\n return 'event';\n }\n }\n\n protected setupItemsObservable(): Observable<ComputedPropertyDefinition[]> {\n return fromTriggerOnce<ComputedPropertyDefinition>(this.router, this.refresh$, [\n getInjectedHooks<ComputedPropertyDefinition>(HOOK_COMPUTED_PROPERTY, this.injectors),\n () => this.factories,\n stateToFactory(this.state$)\n ]).pipe(distinctUntilChanged(), shareReplay(1));\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { IManagedObject, InventoryService, IResultList } from '@c8y/client';\nimport {\n AlertService,\n AssetDefinition,\n AssetDefinitionsService,\n AssetTypesRealtimeService,\n C8yJsonSchemaProperty,\n FeatureCacheService,\n GroupService\n} from '@c8y/ngx-components';\nimport { isObjectLike, isEmpty, forOwn, isArray, find, isEqual, omit } from 'lodash-es';\nimport { firstValueFrom } from 'rxjs';\nimport { AssetPropertyType } from './asset-properties.model';\nimport { ComputedPropertiesService } from './computed-properties.service';\nimport { TranslateService } from '@ngx-translate/core';\n\n/**\n * Service for managing asset properties.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class AssetPropertiesService {\n private readonly FRAGMENTS_TO_OMIT = [\n 'additionParents',\n 'assetParents',\n 'deviceParents',\n 'childAdditions',\n 'childAssets',\n 'childDevices',\n 'c8y_IsDevice',\n '__children',\n 'c8y_ui',\n 'self',\n 'parent',\n 'c8y_DataPoint',\n 'c8y_Kpi_Migrated',\n /^c8y_Dashboard!\\d+/\n ];\n private readonly MODEL_API_FEATURE_KEY = 'dtm.asset-api';\n\n private inventoryService = inject(InventoryService);\n private assetTypesRealtimeService = inject(AssetTypesRealtimeService);\n private groupService = inject(GroupService);\n private alert = inject(AlertService);\n private computedPropertiesService = inject(ComputedPropertiesService);\n private featureCacheService = inject(FeatureCacheService);\n private assetDefinitionsService = inject(AssetDefinitionsService);\n private translateService = inject(TranslateService);\n\n /**\n * Filters added properties to only include those compatible with the given asset.\n * Currently only checks compatibility for computed properties.\n * @param allAddedProperties All properties that have been added by the user\n * @param asset The current asset context\n * @returns Promise resolving to properties compatible with the asset\n */\n async filterCompatibleProperties(\n allAddedProperties: AssetPropertyType[],\n asset: IManagedObject\n ): Promise<AssetPropertyType[]> {\n if (!asset || !allAddedProperties.length) {\n return allAddedProperties;\n }\n\n let availableComputedPropertyNames: Set<string> | null = null;\n const computedProperties = allAddedProperties.filter(prop => prop.computed);\n\n if (computedProperties.length > 0) {\n const availableComputedProperties = await this.computedPropertiesService.getByContext(asset);\n availableComputedPropertyNames = new Set(\n availableComputedProperties.map(({ prop }) => prop.name)\n );\n }\n\n const compatibleProperties: AssetPropertyType[] = [];\n\n for (const property of allAddedProperties) {\n if (await this.isPropertyAvailable(property, availableComputedPropertyNames)) {\n compatibleProperties.push(property);\n }\n }\n\n return compatibleProperties;\n }\n\n /**\n * Retrieves properties for an asset.\n * First, it tries to get properties from definitions API (if enabled),\n * then falls back to asset library.\n * @param asset The asset for which to retrieve custom properties.\n * @returns A promise resolving to the list of custom properties.\n */\n async getAssetProperties(asset: IManagedObject): Promise<AssetPropertyType[]> {\n if (!asset?.type) {\n return [];\n }\n const isModelApiEnabled = await firstValueFrom(\n this.featureCacheService.getFeatureState(this.MODEL_API_FEATURE_KEY)\n );\n\n if (isModelApiEnabled) {\n try {\n const assetDefinition = await this.assetDefinitionsService.getByIdentifier(asset.type);\n if (assetDefinition) {\n return this.categorizePropertiesFromDefinition(assetDefinition);\n }\n } catch {\n // do nothing- fallback to asset library\n }\n }\n\n const assetType = await firstValueFrom(\n this.assetTypesRealtimeService.getAssetTypeByName$(asset.type)\n );\n if (assetType) {\n const { data } = await this.inventoryService.childAdditionsList(assetType, {\n pageSize: 2000,\n query: \"$filter=(has('c8y_IsAssetProperty'))\"\n });\n return this.categorizeCustomProperties(data);\n } else {\n return [];\n }\n }\n\n /**\n * Retrieves the initial set of properties for an asset, based on its type.\n * @param asset The asset for which to retrieve properties.\n * @returns A promise resolving to the list of initial properties.\n */\n async getInitialProperties(asset: IManagedObject): Promise<AssetPropertyType[]> {\n if (!asset) {\n return [];\n } else if (this.groupService.isDevice(asset)) {\n return await this.getDeviceProperties(asset);\n } else if (this.groupService.isGroup(asset) && !this.groupService.isAsset(asset)) {\n return await this.getGroupProperties(asset);\n } else if (this.groupService.isAsset(asset)) {\n return await this.getAssetProperties(asset);\n }\n return [];\n }\n\n /**\n * Retrieves properties for a device asset.\n * @param asset The device asset for which to retrieve properties.\n * @returns A promise resolving to the list of device properties.\n */\n async getDeviceProperties(asset: IManagedObject): Promise<AssetPropertyType[]> {\n return this.getManagedObjectProperties(asset);\n }\n\n /**\n * Retrieves properties for a group asset.\n * @param asset The group asset for which to retrieve properties.\n * @returns A promise resolving to the list of group properties.\n */\n async getGroupProperties(asset: IManagedObject): Promise<AssetPropertyType[]> {\n return this.getManagedObjectProperties(asset);\n }\n\n /**\n * Categorizes custom properties into simple and complex types.\n * @param properties The custom properties to categorize.\n * @returns The categorized custom properties.\n */\n categorizeCustomProperties(\n properties: IManagedObject[] | AssetPropertyType[]\n ): AssetPropertyType[] {\n const { simple, complex } = properties.reduce(\n (acc, property) => {\n const schema = property.c8y_JsonSchema.properties[property.name];\n if (schema.type === 'object') {\n acc.complex.push(property);\n } else {\n acc.simple.push(property);\n }\n return acc;\n },\n { simple: [], complex: [] }\n );\n\n return [...simple, ...complex] as AssetPropertyType[];\n }\n\n /**\n * Categorizes and flattens hierarchical properties into simple and complex types.\n * @param properties The hierarchical properties to categorize and flatten.\n * @returns The categorized and flattened properties.\n */\n categorizeAndFlattenHierarchicalProperties(properties: AssetPropertyType[]): {\n computed: AssetPropertyType[];\n simple: AssetPropertyType[];\n complex: AssetPropertyType[];\n } {\n const sortedProperties = [...properties].sort((a, b) => {\n const aLabel = (a.label || a.name || '').toLowerCase();\n const bLabel = (b.label || b.name || '').toLowerCase();\n return aLabel.localeCompare(bLabel);\n });\n const result = sortedProperties.reduce(\n (acc, property) => {\n property.active = false;\n if (property.computed) {\n acc.computed.push(property);\n } else if (this.isComplexProperty(property)) {\n acc.complex.push(property);\n this.addNestedProperties(property, acc.complex, true);\n } else {\n acc.simple.push(property);\n }\n return acc;\n },\n { computed: [], simple: [], complex: [] }\n );\n return result;\n }\n\n /**\n * Checks if a property is complex (i.e., has nested properties).\n * @param property The property to check.\n * @returns True if the property is complex, false otherwise.\n */\n isComplexProperty(property: AssetPropertyType): boolean {\n return (\n property.c8y_JsonSchema?.properties[property.name]?.type === 'object' ||\n property.properties !== undefined\n );\n }\n\n /**\n * Checks if property is available based on provided available computed property names.\n * @param property The property to check.\n * @param availableComputedPropertyNames Set of available computed property names.\n * @returns True if the property is available, false otherwise.\n */\n async isPropertyAvailable(\n property: AssetPropertyType,\n availableComputedPropertyNames: Set<string>\n ): Promise<boolean> {\n if (property.computed) {\n if (!availableComputedPropertyNames) {\n return false;\n }\n return availableComputedPropertyNames.has(property.name);\n }\n\n return true;\n }\n\n /**\n * Checks if two properties match for selection purposes.\n * @param property1 First property to compare.\n * @param property2 Second property to compare.\n * @param omitProperties Optional list of property keys to omit from comparison (e.g., ['instanceId','configuredAssetId']).\n * @returns True if properties match.\n */\n propertiesMatch(\n property1: AssetPropertyType,\n property2: AssetPropertyType,\n omitProperties: (keyof AssetPropertyType)[] = []\n ): boolean {\n if (property1.name === property2.name) {\n const propertiesToOmit: (keyof AssetPropertyType)[] = ['active', ...omitProperties];\n const areEqual = isEqual(\n omit(property1, propertiesToOmit),\n omit(property2, propertiesToOmit)\n );\n return areEqual;\n }\n return false;\n }\n\n /**\n * Retrieves custom properties from the properties library, optionally filtered by search text.\n * @param searchText Optional search text to filter properties.\n * @returns A promise resolving to the list of properties and paging information.\n */\n async getPropertiesFromPropertiesLibrary(searchText?: string) {\n const propertiesFromLibrary: AssetPropertyType[] = [];\n const { data: propertiesMOs, paging } =\n await this.requestPropertiesFromPropertiesLibrary(searchText);\n propertiesMOs.forEach(prop => {\n const name = Object.keys(prop.c8y_JsonSchema.properties)[0];\n propertiesFromLibrary.push({\n c8y_JsonSchema: prop.c8y_JsonSchema,\n label: prop.name,\n name,\n type: prop.c8y_JsonSchema.properties[name].type\n });\n });\n return { propertiesFromLibrary, paging };\n }\n\n /**\n * Filters properties with hierarchical search logic:\n * - Simple properties: match label or name\n * - Child properties: match child label/name AND if matches- match also parent\n * - Complex properties: match parent label/name AND if matches- all children\n *\n * @example\n * // Search \"or\" → matches children \"Major\", \"Minor\" → includes parent + matching children\n * filterPropertiesWithHierarchy([\n * { name: 'c8y_ActiveAlarmsStatus', label: 'Active alarms status', type: 'object', ... },\n * { name: 'major', label: 'Major', keyPath: ['c8y_ActiveAlarmsStatus', 'major'], ... },\n * { name: 'minor', label: 'Minor', keyPath: ['c8y_ActiveAlarmsStatus', 'minor'], ... },\n * { name: 'critical', label: 'Critical', keyPath: ['c8y_ActiveAlarmsStatus', 'critical'], ... }\n * ], 'or')\n * // Returns: [\n * { name: 'c8y_ActiveAlarmsStatus', label: 'Active alarms status', type: 'object', ... },\n * { name: 'major', label: 'Major', keyPath: ['c8y_ActiveAlarmsStatus', 'major'], ... },\n * { name: 'minor', label: 'Minor', keyPath: ['c8y_ActiveAlarmsStatus', 'minor'], ... }\n * ]\n *\n * @example\n * // Search \"address\" → matches parent → includes parent + all children\n * filterPropertiesWithHierarchy([\n * { name: 'c8y_Address', label: 'Address', type: 'object', ... },\n * { name: 'street', label: 'Street', keyPath: ['c8y_Address', 'street'], ... },\n * { name: 'city', label: 'City', keyPath: ['c8y_Address', 'city'], ... }\n * ], 'address')\n * // Returns: [\n * { name: 'c8y_Address', label: 'Address', type: 'object', ... },\n * { name: 'street', label: 'Street', keyPath: ['c8y_Address', 'street'], ... },\n * { name: 'city', label: 'City', keyPath: ['c8y_Address', 'city'], ... }\n * ]\n *\n * @param flattenedProperties All flattened properties (simple and complex)\n * @param searchTerm Search term (case-insensitive, already lowercased)\n * @returns Filtered properties matching the search term\n */\n filterPropertiesWithHierarchy(\n flattenedProperties: AssetPropertyType[],\n searchTerm: string\n ): AssetPropertyType[] {\n if (!searchTerm) {\n return flattenedProperties;\n }\n\n const lowercasedSearchTerm = searchTerm.toLowerCase();\n\n // Extract complex properties for parent lookups\n const complexProperties = flattenedProperties.filter(prop => this.isComplexProperty(prop));\n const parentMap = new Map(complexProperties.map(p => [p.name, p]));\n\n return flattenedProperties\n .map(prop => {\n if (!this.isComplexProperty(prop) && !prop.keyPath) {\n return this.matchesSimpleProperty(prop, lowercasedSearchTerm);\n } else if (!this.isComplexProperty(prop) && prop.keyPath) {\n return this.matchesChildProperty(prop, lowercasedSearchTerm, parentMap);\n } else {\n return this.matchesComplexProperty(prop, lowercasedSearchTerm);\n }\n })\n .filter(prop => prop !== null);\n }\n\n /**\n * Checks if a simple property (which is not a child of a complex property) matches the search term.\n * @param prop The property to check.\n * @param searchTerm The search term (already lowercased).\n * @returns The property if it matches, otherwise null.\n */\n private matchesSimpleProperty(\n prop: AssetPropertyType,\n searchTerm: string\n ): AssetPropertyType | null {\n const matches =\n prop.name.toLowerCase().includes(searchTerm) ||\n prop.label.toLowerCase().includes(searchTerm) ||\n this.translateService.instant(prop.label).toLowerCase().includes(searchTerm);\n return matches ? prop : null;\n }\n\n /**\n * 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.\n * @param prop The property to check.\n * @param searchTerm The search term (already lowercased).\n * @param parentMap A map of parent complex properties.\n * @returns The property if it matches, otherwise null.\n */\n private matchesChildProperty(\n prop: AssetPropertyType,\n searchTerm: string,\n parentMap: Map<string, AssetPropertyType>\n ): AssetPropertyType | null {\n // Check if child property matches\n const childMatches =\n prop.name.toLowerCase().includes(searchTerm) ||\n prop.label.toLowerCase().includes(searchTerm) ||\n this.translateService.instant(prop.label).toLowerCase().includes(searchTerm);\n if (childMatches) {\n return prop;\n }\n\n // Check if parent complex property matches\n const parentName = prop.keyPath[0];\n const parentProp = parentMap.get(parentName);\n\n if (parentProp) {\n const parentMatches =\n parentProp.name.toLowerCase().includes(searchTerm) ||\n parentProp.label.toLowerCase().includes(searchTerm) ||\n this.translateService.instant(parentProp.label).toLowerCase().includes(searchTerm);\n return parentMatches ? prop : null;\n }\n\n return null;\n }\n\n /**\n * Checks if a complex property matches the search term or if any of its child properties match the search term.\n * @param prop The complex property to check.\n * @param searchTerm The search term (already lowercased).\n * @returns The property if it matches, otherwise null.\n */\n private matchesComplexProperty(\n prop: AssetPropertyType,\n searchTerm: string\n ): AssetPropertyType | null {\n // Complex property\n const parentMatches =\n prop.name.toLowerCase().includes(searchTerm) ||\n prop.label.toLowerCase().includes(searchTerm) ||\n this.translateService.instant(prop.label).toLowerCase().includes(searchTerm);\n if (parentMatches) {\n return prop;\n }\n // If parent doesn't match, check children properties- if any of the children match, then we include the parent complex property\n // Get nested properties from the schema\n const nestedPropertiesObj = prop.c8y_JsonSchema?.properties[prop.name]?.properties;\n\n if (nestedPropertiesObj) {\n // Check if any nested property matches the search term\n const matchingNestedProperty = Object.keys(nestedPropertiesObj).find(key => {\n const nestedProp = nestedPropertiesObj[key];\n return (\n key.toLowerCase().includes(searchTerm) ||\n (nestedProp.title || '').toLowerCase().includes(searchTerm) ||\n (nestedProp.title &&\n this.translateService\n .instant(nestedProp.title || '')\n .toLowerCase()\n .includes(searchTerm))\n );\n });\n if (matchingNestedProperty) {\n return prop;\n } else {\n return null;\n }\n } else {\n return null;\n }\n }\n\n private async requestPropertiesFromPropertiesLibrary(\n searchText?: string\n ): Promise<IResultList<IManagedObject>> {\n let queryFilter = '(type eq c8y_JsonSchema) and (appliesTo.MANAGED_OBJECTS eq true)';\n\n if (searchText?.trim()) {\n const escapedSearchText = searchText.replace(/'/g, \"''\");\n queryFilter += ` and (name eq '*${escapedSearchText}*')`;\n }\n\n const query = `$filter=(${queryFilter})`;\n const filter = {\n pageSize: 20,\n revert: true,\n query,\n withTotalPages: true,\n withTotalElements: true\n };\n try {\n const results = await this.inventoryService.list(filter);\n return results;\n } catch (error) {\n this.alert.addServerFailure(error);\n return null;\n }\n }\n\n private addNestedProperties(\n parentProperty: AssetPropertyType,\n complexProperties: AssetPropertyType[],\n sortChildren = false\n ): void {\n const schema = parentProperty.c8y_JsonSchema?.properties[parentProperty.name];\n if (!schema?.properties) {\n return;\n }\n this.flattenProperties(schema, complexProperties, parentProperty.name, [], sortChildren);\n }\n\n private flattenProperties(\n schema: C8yJsonSchemaProperty,\n result: AssetPropertyType[],\n parentName: string,\n parentPath: string[] = [],\n sortChildren = false\n ): void {\n const properties = schema.properties?.[parentName]?.properties || schema.properties;\n let entries = Object.entries(properties);\n if (sortChildren) {\n entries = entries.sort((a, b) => {\n const aLabel = (a[1].title || a[1].label || a[1].name || a[0] || '').toLowerCase();\n const bLabel = (b[1].title || b[1].label || b[1].name || b[0] || '').toLowerCase();\n return aLabel.localeCompare(bLabel);\n });\n }\n entries.forEach(([key, property]) => {\n const path = parentPath.includes(parentName) ? parentPath : [...parentPath, parentName];\n result.push({\n ...property,\n name: key,\n label: property.label || property.title || key,\n keyPath: [...path, key]\n } as AssetPropertyType);\n if (property.properties) {\n this.flattenProperties(\n { properties: { [key]: property } } as C8yJsonSchemaProperty,\n result,\n key,\n [...path, key],\n sortChildren\n );\n }\n });\n }\n\n private getManagedObjectProperties(asset: IManagedObject): AssetPropertyType[] {\n return this.extractFragments(asset);\n }\n\n private extractFragments(object: IManagedObject): AssetPropertyType[] {\n const properties = [];\n for (const [key, value] of Object.entries(object)) {\n if (\n this.shouldSkipFragment(key) ||\n isArray(value) ||\n (isObjectLike(value) && isEmpty(value))\n ) {\n continue;\n }\n const newProp = {\n label: key,\n name: key,\n type: isObjectLike(value) ? 'object' : 'string',\n isEditable: true,\n c8y_JsonSchema: {\n properties: {\n [key]: {\n key: key,\n type: isObjectLike(value) ? 'object' : 'string',\n label: key,\n properties: {}\n }\n }\n }\n };\n\n if (isObjectLike(value)) {\n this.addPropertyItem(newProp.c8y_JsonSchema.properties[key].properties, value);\n }\n\n properties.push(newProp);\n }\n\n return properties;\n }\n\n private shouldSkipFragment(key: string): boolean {\n return !!find(this.FRAGMENTS_TO_OMIT, (fragmentToOmit: string | RegExp) => {\n if (fragmentToOmit instanceof RegExp) {\n return fragmentToOmit.test(key);\n }\n return fragmentToOmit === key;\n });\n }\n\n private addPropertyItem(properties, object) {\n if (!properties) {\n return;\n }\n forOwn(object, (value, key) => {\n properties[key] = { title: key, type: value ? typeof value : 'string' };\n if (isObjectLike(value)) {\n properties[key].type = 'object';\n this.addPropertyItem(properties[key]['propertie