UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines 198 kB
{"version":3,"file":"c8y-ngx-components-widgets-implementations-asset-table.mjs","sources":["../../widgets/implementations/asset-table/columns/date.cell-renderer.component.ts","../../widgets/implementations/asset-table/asset-table.model.ts","../../widgets/implementations/asset-table/columns/date.asset-table-column.ts","../../widgets/implementations/asset-table/columns/icon.cell-renderer.component.ts","../../widgets/implementations/asset-table/columns/icon.asset-table-column.ts","../../widgets/implementations/asset-table/columns/operation.cell-renderer.component.ts","../../widgets/implementations/asset-table/columns/operation.asset-table-column.ts","../../widgets/implementations/asset-table/columns/computed.cell-renderer.component.ts","../../widgets/implementations/asset-table/columns/computed.asset-table.column.ts","../../widgets/implementations/asset-table/columns/default.cell-renderer.component.ts","../../widgets/implementations/asset-table/columns/default.asset-table.column.ts","../../widgets/implementations/asset-table/asset-table.service.ts","../../widgets/implementations/asset-table/asset-table-auto-refresh/asset-table-auto-refresh.component.ts","../../widgets/implementations/asset-table/asset-table-auto-refresh/asset-table-auto-refresh.component.html","../../widgets/implementations/asset-table/asset-table-config/asset-table-widget-config.component.ts","../../widgets/implementations/asset-table/asset-table-config/asset-table-widget-config.component.html","../../widgets/implementations/asset-table/asset-table-view/asset-table-widget-view.component.ts","../../widgets/implementations/asset-table/asset-table-view/asset-table-widget-view.component.html","../../widgets/implementations/asset-table/icon-render-type-modal/icon-render-type-modal.component.ts","../../widgets/implementations/asset-table/icon-render-type-modal/icon-render-type-modal.component.html","../../widgets/implementations/asset-table/asset-table-column-settings/asset-table-column-settings.component.ts","../../widgets/implementations/asset-table/asset-table-column-settings/asset-table-column-settings.component.html","../../widgets/implementations/asset-table/c8y-ngx-components-widgets-implementations-asset-table.ts"],"sourcesContent":["import { Component, OnInit } from '@angular/core';\nimport { CellRendererContext, DatePipe } from '@c8y/ngx-components';\nimport { ColumnUtilService } from '@c8y/ngx-components/device-grid';\n\n@Component({\n template: `\n @if (isLink) {\n <a [href]=\"href\">\n {{ context.value | c8yDate }}\n </a>\n } @else {\n {{ context.value | c8yDate }}\n }\n `,\n selector: 'c8y-date-cell-renderer',\n imports: [DatePipe]\n})\nexport class DateCellRendererComponent implements OnInit {\n isLink = false;\n href: string | null = null;\n\n constructor(\n public context: CellRendererContext,\n public columnUtilService: ColumnUtilService\n ) {}\n\n ngOnInit(): void {\n this.isLink = !!this.context?.property?.isLink;\n this.href = this.isLink ? this.columnUtilService.getHref(this.context.item) : null;\n }\n}\n","import {\n BaseColumn,\n CustomColumn,\n ColumnConfig,\n CustomColumnType,\n DisplayOptions,\n WidgetDisplaySettings,\n AssetTableExtendedColumn,\n ComputedConfig,\n IconConfigItem,\n AssetColumnOperationType,\n WidgetSettings\n} from '@c8y/ngx-components';\nimport { AlarmsDeviceGridColumn } from '@c8y/ngx-components/device-grid';\nimport { DefaultAssetTableGridColumn } from './columns/default.asset-table.column';\nimport { IIdentified } from '@c8y/client';\nimport { OperationAssetTableGridColumn } from './columns/operation.asset-table-column';\nimport { AssetPropertyType } from '@c8y/ngx-components/asset-properties';\nimport { gettext } from '@c8y/ngx-components/gettext';\n\nexport interface AssetTableWidgetConfig {\n columnSortOrders?: Record<string, 'asc' | 'desc' | ''>;\n columns?: DefaultAssetTableGridColumn[];\n configurableColumnsEnabled?: boolean;\n device?: IIdentified;\n displayOptions?: Partial<DisplayOptions>;\n displaySettings?: WidgetDisplaySettings;\n filterPredicate?: Record<string, any>;\n isAutoRefreshEnabled?: boolean;\n operationColumns?: OperationAssetTableGridColumn[];\n refreshInterval?: number;\n options?: any; // legacy configuration for the asset table properties.\n refreshOption?: AssetTableRefreshOption;\n includeDescendants?: boolean;\n selectedProperties?: AssetPropertyType[];\n showAsLink?: boolean;\n showStatusIcon?: boolean;\n showIconAndValue?: boolean;\n settings?: WidgetSettings;\n title?: string;\n columnOrder?: AssetTableExtendedColumn[];\n isDeviceAssetSelected?: boolean;\n widgetInstanceGlobalAutoRefreshContext?: boolean;\n widgetInstanceGlobalTimeContext?: boolean;\n}\n\nexport type AssetTableRefreshOption = 'interval' | 'global-interval';\n\nexport const GLOBAL_INTERVAL_OPTION: AssetTableRefreshOption = 'global-interval';\nexport const DEFAULT_INTERVAL_VALUE = 30_000;\n\nexport interface ComparisonOption {\n label: string;\n value: string;\n sign: string;\n}\n\nexport const COMPARISON_OPTIONS: Record<string, ComparisonOption[]> = {\n number: [\n { label: gettext('Greater than'), value: 'GREATER_THAN', sign: '>' },\n { label: gettext('Less than'), value: 'LESS_THAN', sign: '<' },\n { label: gettext('Equal'), value: 'EQUAL', sign: '===' },\n { label: gettext('Not equal'), value: 'NOT_EQUAL', sign: '!==' }\n ],\n date: [\n { label: gettext('Before`date`'), value: 'BEFORE', sign: '<' },\n { label: gettext('After`date`'), value: 'AFTER', sign: '>' },\n { label: gettext('On`date`'), value: 'ON', sign: '===' }\n ],\n string: [\n { label: gettext('Contains'), value: 'CONTAINS', sign: 'includes' },\n { label: gettext('Equal'), value: 'EQUAL', sign: '===' },\n { label: gettext('Not equal'), value: 'NOT_EQUAL', sign: '!==' },\n { label: gettext('Starts with'), value: 'STARTS_WITH', sign: 'startsWith' },\n { label: gettext('Ends with'), value: 'ENDS_WITH', sign: 'endsWith' }\n ],\n boolean: [\n { label: gettext('Is true'), value: 'IS_TRUE', sign: 'true' },\n { label: gettext('Is false'), value: 'IS_FALSE', sign: 'false' }\n ]\n};\n\nexport type AssetColumnType = 'alarm' | 'date' | 'icon' | 'computed' | 'default' | 'operation';\n\nexport type AssetTableQuery = {\n __filter: {\n [key: string]: any;\n __and?: any[];\n };\n __orderby: Array<{ [key: string]: number }>;\n [key: string]: any;\n};\n\nexport class BaseColumnExtended extends BaseColumn {\n iconConfig?: Array<IconConfigItem>;\n isLink?: boolean;\n command?: object;\n isOperation?: boolean;\n operationType?: AssetColumnOperationType;\n computedConfig?: ComputedConfig;\n showIconAndValue?: boolean;\n buttonLabel?: string;\n type?: AssetColumnType;\n}\n\nexport class CustomColumnExtended extends CustomColumn {\n iconConfig?: Array<IconConfigItem>;\n isLink?: boolean;\n command?: object;\n isOperation?: boolean;\n operationType?: AssetColumnOperationType;\n computedConfig?: ComputedConfig;\n showIconAndValue?: boolean;\n columnOrder?: [];\n}\n\nexport interface ColumnConfigExtended extends ColumnConfig {\n /** Column header title */\n header?: string;\n\n /** The path in a row item to read the cell value from. */\n path?: string;\n\n /**\n * Icon configuration for the column when renderType is 'icon'.\n */\n iconConfig?: Array<IconConfigItem>;\n\n /** Configuration for computed column */\n computedConfig?: ComputedConfig;\n\n /** Whether the column is a link. */\n isLink?: boolean;\n\n /** The column operationType. E.g. 'maintenance' */\n operationType?: AssetColumnOperationType;\n\n /** Command to be executed */\n command?: any;\n\n /** Whether to show both icon and value in the cell */\n showIconAndValue?: boolean;\n\n /** Button label for operation columns */\n buttonLabel?: string;\n\n /** e.g. 'Restart Device', 'Firmware Update', 'Send Command' */\n operation?: string;\n}\n\nexport interface CustomColumnConfigExtended extends ColumnConfigExtended {\n /** JSON path to the managed object property to be displayed */\n path: string;\n\n /** Column header title */\n header: string;\n\n /** Flag to identify custom columns */\n custom: boolean;\n\n /** Column type of the custom column*/\n type: CustomColumnType;\n}\n\nexport class AlarmsDeviceGridColumnExtended extends AlarmsDeviceGridColumn {\n // Add any extra fields from BaseColumnExtended\n iconConfig?: any;\n isLink?: boolean;\n command?: object;\n isOperation?: boolean;\n operationType?: AssetColumnOperationType;\n computedConfig?: any;\n showIconAndValue?: boolean;\n\n constructor(initialColumnConfig?: ColumnConfigExtended) {\n super(initialColumnConfig);\n }\n}\n","import { FormGroup } from '@angular/forms';\nimport { DateCellRendererComponent } from './date.cell-renderer.component';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { BaseColumnExtended, ColumnConfigExtended } from '../asset-table.model';\n\nexport class DateAssetTableGridColumn extends BaseColumnExtended {\n constructor(initialColumnConfig?: ColumnConfigExtended) {\n super(initialColumnConfig);\n\n this.path = this.path;\n this.name = this.name;\n this.header = this.header;\n this.type = 'date';\n this.cellRendererComponent = DateCellRendererComponent;\n\n this.filterable = true;\n this.filteringConfig = {\n fields: [\n {\n fieldGroup: [\n {\n type: 'date-time',\n key: 'after',\n templateOptions: {\n label: gettext('From date')\n },\n expressionProperties: {\n 'templateOptions.maxDate': (model: any) => model?.before\n }\n },\n {\n type: 'date-time',\n key: 'before',\n templateOptions: {\n label: gettext('To date')\n },\n expressionProperties: {\n 'templateOptions.minDate': (model: any) => model?.after\n }\n }\n ],\n validators: {\n atLeastOneFilled: {\n expression: (formGroup: FormGroup) => {\n const after = formGroup.get('after')?.value;\n const before = formGroup.get('before')?.value;\n return !!after || !!before;\n },\n message: gettext('Specify at least one date.')\n }\n }\n }\n ],\n formGroup: new FormGroup({}),\n getFilter: model => {\n const filter: any = {};\n const dates = model as { after?: string; before?: string };\n if (dates && (dates.after || dates.before)) {\n filter.__and = [];\n if (dates.after) {\n const after = this.formatDate(dates.after);\n filter.__and.push({\n [`${this.path}.date`]: { __gt: after }\n });\n }\n if (dates.before) {\n const before = this.formatDate(dates.before);\n filter.__and.push({\n [`${this.path}.date`]: { __lt: before }\n });\n }\n }\n return filter;\n }\n };\n\n this.sortable = true;\n this.sortingConfig = {\n pathSortingConfigs: [{ path: `${this.path}.date` }]\n };\n }\n\n protected formatDate(dateToFormat: string): string {\n return new Date(dateToFormat).toISOString();\n }\n}\n","import { AsyncPipe, NgTemplateOutlet } from '@angular/common';\nimport { Component, runInInjectionContext } from '@angular/core';\nimport { CellRendererContext, IconDirective, AssetTableExtendedColumn } from '@c8y/ngx-components';\nimport {\n ComputedPropertiesService,\n ComputedPropertyContextValue\n} from '@c8y/ngx-components/asset-properties';\nimport { ColumnUtilService } from '@c8y/ngx-components/device-grid';\nimport { from, isObservable, Observable, of, switchMap } from 'rxjs';\n\n@Component({\n template: ` <ng-template #iconAndValueTemplate let-value>\n @if (matchedCondition(value); as match) {\n <i class=\"m-r-4\" [c8yIcon]=\"match.icon\" [style.color]=\"match.color\"></i>\n @if (context.property.showIconAndValue) {\n <span class=\"text-truncate\" [title]=\"value\">{{ value }}</span>\n }\n } @else {\n <span class=\"text-truncate\" [title]=\"value\">{{ value }}</span>\n }\n </ng-template>\n\n @if (context?.property?.isLink) {\n <a class=\"d-flex a-i-center text-truncate\" [href]=\"columnUtilService.getHref(context.item)\">\n @if (computed$ | async; as computed) {\n <ng-container\n *ngTemplateOutlet=\"iconAndValueTemplate; context: { $implicit: computed }\"\n ></ng-container>\n } @else {\n <ng-container\n *ngTemplateOutlet=\"\n iconAndValueTemplate;\n context: {\n $implicit: resolveValue(context)\n }\n \"\n ></ng-container>\n }\n </a>\n } @else {\n <div class=\"d-flex a-i-center text-truncate\">\n @if (computed$ | async; as computed) {\n <ng-container\n *ngTemplateOutlet=\"iconAndValueTemplate; context: { $implicit: computed }\"\n ></ng-container>\n } @else {\n <ng-container\n *ngTemplateOutlet=\"\n iconAndValueTemplate;\n context: {\n $implicit: resolveValue(context)\n }\n \"\n ></ng-container>\n }\n </div>\n }`,\n selector: 'c8y-icon-cell-renderer',\n imports: [AsyncPipe, IconDirective, NgTemplateOutlet]\n})\nexport class IconCellRendererComponent {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n computed$: Observable<any> | null = null;\n\n constructor(\n public context: CellRendererContext,\n public computedPropertyService: ComputedPropertiesService,\n public columnUtilService: ColumnUtilService\n ) {}\n\n async ngOnInit() {\n if (this.context.property.computedConfig) {\n this.computed$ = this.getComputedValue(this.context.property, this.context.item);\n }\n }\n\n getComputedValue(\n property: AssetTableExtendedColumn,\n context: ComputedPropertyContextValue\n ): Observable<any> {\n const propertyName = property.computedConfig?.__propertyName || property.name;\n return from(this.computedPropertyService.getByName(propertyName)).pipe(\n switchMap(definition => {\n let value: any | Observable<any> | Promise<any> = '-';\n runInInjectionContext(definition.injector, () => {\n if (property.computedConfig?.dp?.length > 0) {\n property.computedConfig.dp[0].__target = { ...context };\n }\n value = definition.value({\n config: property.computedConfig,\n context,\n metadata: { mode: 'singleValue' }\n });\n });\n\n if (isObservable(value) || value instanceof Promise) {\n return value;\n } else {\n return of(value);\n }\n })\n );\n }\n\n matchedCondition(cellValue: string | number | Date) {\n const iconConfigs = this.context?.property?.iconConfig;\n if (!Array.isArray(iconConfigs)) return null;\n\n return (\n iconConfigs.find(config => {\n const { comparison, value } = config;\n if (!comparison || value === undefined || value === null) return false;\n\n return this.evaluateCondition(cellValue, comparison.value, value);\n }) ?? null\n );\n }\n\n resolveValue(context: CellRendererContext) {\n const { item, property } = context;\n if (!item || !property) {\n return undefined;\n }\n\n const path = property.path;\n if (Array.isArray(path)) {\n return path.reduce((acc, key) => acc?.[key], item);\n }\n\n return item[path];\n }\n\n private evaluateCondition(\n cellValue: string | number | Date | boolean,\n operator: string,\n value: string\n ): boolean {\n switch (operator) {\n case 'GREATER_THAN':\n return cellValue > value;\n case 'LESS_THAN':\n return cellValue < value;\n case 'EQUAL':\n return cellValue === value;\n case 'NOT_EQUAL':\n return cellValue !== value;\n case 'CONTAINS':\n return typeof cellValue === 'string' && cellValue.includes(value);\n case 'STARTS_WITH':\n return typeof cellValue === 'string' && cellValue.startsWith(value);\n case 'ENDS_WITH':\n return typeof cellValue === 'string' && cellValue.endsWith(value);\n case 'IS_TRUE':\n return cellValue === true;\n case 'IS_FALSE':\n return cellValue === false;\n case 'BEFORE':\n return new Date(cellValue as number | Date) < new Date(value);\n case 'AFTER':\n return new Date(cellValue as number | Date) > new Date(value);\n case 'ON':\n return (\n new Date(cellValue as number | Date).toDateString() === new Date(value).toDateString()\n );\n default:\n return false;\n }\n }\n}\n","import { IconCellRendererComponent } from './icon.cell-renderer.component';\nimport { BaseColumnExtended, ColumnConfigExtended } from '../asset-table.model';\n\nexport class IconAssetTableGridColumn extends BaseColumnExtended {\n constructor(initialColumnConfig?: ColumnConfigExtended) {\n super(initialColumnConfig);\n\n this.path = this.path;\n this.name = this.name;\n this.header = this.header;\n this.computedConfig = this.computedConfig;\n this.type = 'icon';\n this.cellRendererComponent = IconCellRendererComponent;\n this.iconConfig = this.iconConfig;\n this.showIconAndValue = this.showIconAndValue;\n\n this.filterable = false;\n this.sortable = false;\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';\nimport { AlertService, CellRendererContext, CommonModule } from '@c8y/ngx-components';\nimport { InventoryService, OperationService } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Component({\n selector: 'c8y-operation-cell-renderer',\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (shouldShowButton()) {\n <button class=\"btn btn-primary btn-sm\" type=\"button\" (click)=\"onOperationClick()\">\n {{ context.property?.name ?? '' | translate }}\n </button>\n } @else {\n <span class=\"text-muted\">-</span>\n }\n `,\n imports: [CommonModule]\n})\nexport class OperationCellRendererComponent {\n private readonly operations = inject(OperationService);\n private readonly alertService = inject(AlertService);\n private readonly inventoryService = inject(InventoryService);\n private readonly translateService = inject(TranslateService);\n readonly context = inject(CellRendererContext);\n\n readonly shouldShowButton = computed(() => {\n const operationType = this.context.property?.operationType;\n const device = this.context.item;\n\n if (operationType !== 'maintenance') {\n return true;\n }\n\n return this.canSwitchResponseInterval(device);\n });\n\n async onOperationClick() {\n const operationType = this.context.property?.operationType;\n const device = this.context.item;\n\n try {\n if (operationType === 'maintenance') {\n let payload;\n\n if (this.canSwitchResponseInterval(device)) {\n payload = {\n id: device.id,\n c8y_RequiredAvailability: {\n responseInterval: -device.c8y_RequiredAvailability.responseInterval\n }\n };\n } else {\n payload = { id: device.id, c8y_RequiredAvailability: null };\n }\n\n await this.inventoryService.update(payload);\n this.alertService.success(gettext('Maintenance mode toggled.'));\n } else {\n const deviceId = this.context.value;\n const commandBody =\n typeof this.context.property.command === 'string'\n ? JSON.parse(this.context.property.command)\n : this.context.property.command;\n\n const body = { deviceId, ...commandBody };\n await this.operations.create(body);\n this.alertService.success(gettext('Operation created.'));\n }\n } catch (error) {\n this.alertService.danger(\n this.translateService.instant(\n gettext('Failed to execute operation: \"{{ errorMessage }}\"'),\n {\n errorMessage: this.translateService.instant(error.message)\n }\n )\n );\n }\n }\n\n private canSwitchResponseInterval(device: any): boolean {\n return (\n device &&\n device.c8y_RequiredAvailability &&\n device.c8y_RequiredAvailability.responseInterval &&\n parseInt(device.c8y_RequiredAvailability.responseInterval, 10) !== 0\n );\n }\n}\n","import { OperationCellRendererComponent } from './operation.cell-renderer.component';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { BaseColumnExtended, ColumnConfigExtended } from '../asset-table.model';\n\nexport class OperationAssetTableGridColumn extends BaseColumnExtended {\n constructor(initialColumnConfig?: ColumnConfigExtended) {\n super(initialColumnConfig);\n\n this.path = this.path;\n this.name = this.name;\n this.header = this.header || gettext('Operation');\n this.type = 'operation';\n this.cellRendererComponent = OperationCellRendererComponent;\n this.command = this.command;\n this.isOperation = true;\n this.operationType = this.operationType || 'operation';\n\n this.filterable = false;\n this.sortable = false;\n }\n}\n","import { AsyncPipe } from '@angular/common';\nimport { Component, runInInjectionContext } from '@angular/core';\nimport {\n ComputedPropertiesService,\n ComputedPropertyContextValue\n} from '@c8y/ngx-components/asset-properties';\nimport { from, isObservable, map, Observable, of, switchMap } from 'rxjs';\nimport { CellRendererContext, AssetTableExtendedColumn } from '@c8y/ngx-components';\n\n@Component({\n template: `{{ computedValue | async }}`,\n selector: 'c8y-computed-cell-renderer',\n imports: [AsyncPipe]\n})\nexport class ComputedCellRendererComponent {\n computedValue: any;\n\n constructor(\n public context: CellRendererContext,\n public computedPropertyService: ComputedPropertiesService\n ) {}\n\n ngOnInit() {\n this.computedValue = this.getCallbackComputedPropertyValue(\n this.context.property,\n this.context.item\n ).pipe(\n map(value => (typeof value === 'object' && value !== null ? JSON.stringify(value) : value))\n );\n }\n\n private getCallbackComputedPropertyValue(\n property: AssetTableExtendedColumn,\n context: ComputedPropertyContextValue\n ): any | Promise<any> | Observable<any> {\n const propertyName = property.computedConfig?.__propertyName || property.name;\n return from(this.computedPropertyService.getByName(propertyName)).pipe(\n switchMap(definition => {\n let value: any | Observable<any> | Promise<any> = '-';\n runInInjectionContext(definition.injector, () => {\n if (this.context?.property.computedConfig?.dp?.length > 0) {\n this.context.property.computedConfig.dp[0].__target = { ...context };\n }\n value = definition.value({\n config: this.context?.property.computedConfig,\n context,\n metadata: { mode: 'singleValue' }\n });\n });\n\n if (isObservable(value) || value instanceof Promise) {\n return value;\n } else {\n return of(value);\n }\n })\n );\n }\n}\n","import { ComputedCellRendererComponent } from './computed.cell-renderer.component';\nimport { BaseColumnExtended, ColumnConfigExtended } from '../asset-table.model';\n\nexport class ComputedAssetTableGridColumn extends BaseColumnExtended {\n constructor(initialColumnConfig?: ColumnConfigExtended) {\n super(initialColumnConfig);\n\n this.name = this.name;\n this.header = this.header;\n this.computedConfig = this.computedConfig;\n this.visible = this.visible;\n this.type = 'computed';\n this.cellRendererComponent = ComputedCellRendererComponent;\n this.filterable = false;\n this.sortable = true;\n }\n}\n","import { Component, OnInit } from '@angular/core';\nimport { CellRendererContext } from '@c8y/ngx-components';\nimport { ColumnUtilService } from '@c8y/ngx-components/device-grid';\n\n@Component({\n template: `\n @if (isLink) {\n <a [href]=\"href\">{{ displayValue }}</a>\n } @else {\n {{ displayValue }}\n }\n `,\n selector: 'c8y-text-cell-renderer'\n})\nexport class DefaultCellRendererComponent implements OnInit {\n isLink = false;\n href: string | null = null;\n displayValue: string | object | null = null;\n\n constructor(\n public context: CellRendererContext,\n public columnUtilService: ColumnUtilService\n ) {}\n\n ngOnInit(): void {\n this.isLink = !!this.context?.property?.isLink;\n this.href = this.isLink ? this.columnUtilService.getHref(this.context.item) : null;\n\n this.displayValue = this.formatValue(this.context.value);\n }\n\n private formatValue(value: unknown): string {\n if (typeof value === 'object') {\n return JSON.stringify(value);\n }\n\n return String(value);\n }\n}\n","import { DefaultCellRendererComponent } from './default.cell-renderer.component';\nimport { CustomColumnConfigExtended, CustomColumnExtended } from '../asset-table.model';\n\nexport class DefaultAssetTableGridColumn extends CustomColumnExtended {\n constructor(initialColumnConfig?: CustomColumnConfigExtended) {\n super(initialColumnConfig);\n this.type = this.type || 'default';\n this.isLink = this.isLink || false;\n this.cellRendererComponent = DefaultCellRendererComponent;\n this.filterable = true;\n this.sortable = true;\n }\n}\n","import { Injectable, Injector, runInInjectionContext } from '@angular/core';\nimport { IManagedObject, InventoryService, IResultList, QueriesUtil } from '@c8y/client';\nimport { Column, AssetTableExtendedColumn, Pagination } from '@c8y/ngx-components';\nimport { AssetPropertyType, ComputedPropertiesService } from '@c8y/ngx-components/asset-properties';\nimport { assign, get, identity, transform } from 'lodash';\nimport { firstValueFrom, isObservable } from 'rxjs';\nimport { DateAssetTableGridColumn } from './columns/date.asset-table-column';\nimport { IconAssetTableGridColumn } from './columns/icon.asset-table-column';\nimport { OperationAssetTableGridColumn } from './columns/operation.asset-table-column';\nimport { ComputedAssetTableGridColumn } from './columns/computed.asset-table.column';\nimport {\n AlarmsDeviceGridColumnExtended,\n AssetColumnType,\n AssetTableQuery,\n AssetTableWidgetConfig,\n ColumnConfigExtended,\n COMPARISON_OPTIONS,\n ComparisonOption\n} from './asset-table.model';\nimport { DefaultAssetTableGridColumn } from './columns/default.asset-table.column';\nimport { AssetTypeGridColumn } from '@c8y/ngx-components/data-grid-columns';\nimport { gettext } from '@c8y/ngx-components/gettext';\n@Injectable({\n providedIn: 'root'\n})\nexport class AssetTableService {\n private queriesUtil = new QueriesUtil();\n\n constructor(\n private inventoryService: InventoryService,\n private computedPropertiesService: ComputedPropertiesService,\n private injector: Injector\n ) {}\n\n getColumns(\n selectedProperties: AssetPropertyType[],\n operationColumns: ColumnConfigExtended[],\n config?: AssetTableWidgetConfig\n ): AssetTableExtendedColumn[] {\n const firstLinkableIndex = this.getFirstLinkableIndex(selectedProperties, config);\n\n const propertyColumns = this.buildPropertyColumns(\n selectedProperties,\n firstLinkableIndex,\n config\n );\n\n const opColumns = this.buildOperationColumns(operationColumns);\n\n let allColumns = [...propertyColumns, ...opColumns];\n\n if (config?.showStatusIcon) {\n const statusCol = new AssetTypeGridColumn() as AssetTableExtendedColumn;\n statusCol.name = '__status';\n statusCol.__origin = 'status';\n statusCol.__id = 'status';\n statusCol.header = gettext('Status');\n allColumns = [statusCol, ...allColumns];\n }\n\n return this.applySortingAndOrdering(allColumns, config);\n }\n\n buildAssetQueryObj(config: AssetTableWidgetConfig) {\n const assetFilter = config.device\n ? [\n {\n __or: [\n config.includeDescendants\n ? { __isinhierarchyof: config.device.id }\n : config.isDeviceAssetSelected\n ? { id: config.device.id }\n : { __bygroupid: config.device.id }\n ]\n }\n ]\n : [];\n\n return {\n __and: [\n ...assetFilter,\n {\n __or: [\n { __has: 'c8y_IsDevice' },\n { __has: 'c8y_IsAsset' },\n { __has: 'c8y_IsDeviceGroup' }\n ]\n }\n ]\n };\n }\n\n async getAssets(\n config: AssetTableWidgetConfig,\n columns: Column[],\n preConfiguredFilter: Record<string, any> = {},\n pagination: Pagination = { pageSize: 100 }\n ): Promise<IResultList<IManagedObject>> {\n const queryObj = this.buildAssetQueryObj(config);\n const columnQueryObj = this.getQueryObj(columns);\n const preConfigFilterObj = this.extractFilters(preConfiguredFilter);\n const filterQuery = this.buildAndFilterQuery(preConfigFilterObj);\n\n const andConditions: any[] = [queryObj];\n if (Object.keys(columnQueryObj.__filter).length > 0) {\n andConditions.push({ __filter: columnQueryObj.__filter });\n }\n if (Object.keys(preConfigFilterObj).length > 0) {\n andConditions.push({ __filter: filterQuery });\n }\n\n const combinedQueryObj = {\n __filter: {\n __and: andConditions\n },\n __orderby: columnQueryObj.__orderby\n };\n\n const query = this.queriesUtil.buildQuery(combinedQueryObj);\n const result = await this.inventoryService.list({\n query,\n pageSize: pagination.pageSize,\n currentPage: pagination.currentPage || 1,\n withTotalElements: true,\n withTotalPages: true\n });\n\n const allSortedColumns = columns.filter(\n (col: AssetTableExtendedColumn) => col.sortable && col.sortOrder\n ) as AssetTableExtendedColumn[];\n const hasComputedSort = allSortedColumns.some(col => col.type === 'computed');\n if (hasComputedSort && result.data.length > 1) {\n result.data = await this.sortByComputedValues(result.data, allSortedColumns);\n }\n\n return result;\n }\n\n async migrateLegacyProperties(config: AssetTableWidgetConfig): Promise<AssetTableWidgetConfig> {\n if (!config?.options?.properties) {\n return config;\n }\n\n if (config.device) {\n config.device = { ...(await this.inventoryService.detail(config.device.id)).data };\n config.isDeviceAssetSelected = 'c8y_IsDevice' in config.device;\n }\n\n const selectedProperties = [];\n const operationColumns = [];\n\n for (const prop of config.options.properties) {\n const isOperation = prop?.renderType === 'operationButton' || prop?.isAction;\n\n if (isOperation) {\n operationColumns.push({\n header: prop.label,\n operationType:\n prop.renderConfig?.actionType === 'toggleMaintenanceMode' ? 'maintenance' : 'operation',\n operation: null,\n command: JSON.stringify(\n prop?.config?.args?.[0] ?? {\n description: 'Command description',\n c8y_Command: { text: '<command>' }\n },\n null,\n 2\n ),\n buttonLabel: prop.renderConfig?.label ?? prop.label\n });\n continue;\n }\n\n // Map old renderConfig.map -> iconConfig using proper comparison\n if (\n prop.renderConfig?.map &&\n Array.isArray(prop.renderConfig.map) &&\n prop.renderType === 'iconMap'\n ) {\n prop.columnType = 'icon';\n prop.name = prop.name || prop.keyPath?.[0] || prop.label;\n prop.iconConfig = prop.renderConfig.map.map((mapItem: any) => {\n const comparisonObj = this.lookupComparison(mapItem.comparison);\n return {\n comparison: comparisonObj,\n color: mapItem.color,\n icon: mapItem.icon,\n value: mapItem.value\n };\n });\n }\n\n // preserve computed properties and build c8y_JsonSchema\n if (prop.computed) {\n prop.c8y_JsonSchema = this.convertIdToJsonSchema(prop);\n const parts = prop.id.split('!!');\n const propertyName = parts.length > 1 ? parts[1] : prop.id;\n prop.name = propertyName;\n // Use legacy config._id as instanceId so multiple computed columns sharing the\n // same base name (e.g. two \"lastMeasurement\" data-point columns) are kept distinct.\n if (prop.config?._id) {\n prop.instanceId = prop.config._id;\n }\n }\n prop.active = prop.__active;\n // last keypath segment is used as name if name is not provided\n prop.name = prop.name || prop.keyPath?.[prop.keyPath.length - 1] || prop.label;\n\n selectedProperties.push(prop);\n }\n\n return {\n ...config,\n selectedProperties,\n operationColumns,\n refreshInterval: config.refreshInterval ?? 30000,\n refreshOption: config.refreshOption ?? 'interval',\n includeDescendants: config.includeDescendants ?? false,\n isDeviceAssetSelected: config.isDeviceAssetSelected ?? true,\n widgetInstanceGlobalAutoRefreshContext: config.widgetInstanceGlobalAutoRefreshContext ?? null,\n widgetInstanceGlobalTimeContext: config.widgetInstanceGlobalTimeContext ?? false,\n showAsLink: config.showAsLink ?? true,\n showStatusIcon: config.showStatusIcon ?? true,\n displayOptions: {\n ...config.displayOptions,\n gridHeader: false,\n footer: false\n }\n };\n }\n\n migrateToLegacyProperties(config: AssetTableWidgetConfig): AssetTableWidgetConfig {\n if (config.options) {\n delete config.options.properties;\n }\n const properties: any[] = [];\n\n // Convert selectedProperties back to legacy format\n for (const prop of config.selectedProperties || []) {\n const legacyProp: any = { ...prop };\n\n // Restore computed property fields\n if (prop.computed) {\n legacyProp.__active = prop.active;\n legacyProp.id = prop.name ? `c8ySchema!!${prop.name}` : prop.id;\n legacyProp.label = prop.header || prop.label || prop.name;\n legacyProp.type = prop.c8y_JsonSchema?.properties?.[prop.name]?.type || 'string';\n legacyProp.computed = true;\n }\n\n // Restore iconMap\n if (prop.columnType === 'icon' && Array.isArray(prop.iconConfig)) {\n legacyProp.renderType = 'iconMap';\n legacyProp.renderConfig = {\n map: prop.iconConfig.map((icon: any) => ({\n comparison: icon.comparison?.value ?? icon.comparison,\n color: icon.color,\n icon: icon.icon,\n value: icon.value\n }))\n };\n }\n\n // Restore alarm type\n if (prop.columnType === 'alarm') {\n legacyProp.renderType = 'alarm';\n }\n\n // Restore default type\n if (!prop.columnType || prop.columnType === 'default' || prop.columnType === 'base') {\n legacyProp.renderType = 'default';\n }\n\n legacyProp.id = 'c8ySchema!!' + prop.name;\n legacyProp.__active = prop.active;\n\n properties.push(legacyProp);\n }\n\n // Convert operationColumns back to legacy format\n for (const op of config.operationColumns || []) {\n let commandObj;\n if (typeof op.command === 'string') {\n try {\n commandObj = JSON.parse(op.command);\n } catch {\n commandObj = op.command; // fallback if not valid JSON\n }\n } else {\n commandObj = op.command;\n }\n properties.push({\n label: op.header,\n renderType: 'operationButton',\n isAction: true,\n renderConfig: {\n args: Array.isArray(commandObj) ? commandObj : [commandObj],\n label: op.buttonLabel,\n actionType:\n op.operationType === 'maintenance' ? 'toggleMaintenanceMode' : 'createOperation',\n deviceTypes: ['c8y_DeviceGroup', 'c8y_MQTTDevice', 'c8y_DeviceSubgroup']\n },\n keyPath: [op.operationType ? 'createOperation' : 'toggleMaintenanceMode'],\n __active: true\n });\n }\n\n // Build legacy config object\n return {\n ...config,\n options: {\n ...config.options,\n properties\n }\n };\n }\n\n isMigrationNeeded(config: AssetTableWidgetConfig): boolean {\n const legacyProps = config?.options?.properties;\n\n if (!legacyProps?.length) {\n return false;\n }\n\n if (!config.selectedProperties || !config.operationColumns) {\n return true;\n }\n\n const selected = config.selectedProperties ?? [];\n const operations = config.operationColumns ?? [];\n\n if (legacyProps.length !== selected.length + operations.length) {\n return true;\n }\n\n for (const legacyProp of legacyProps) {\n if (legacyProp.renderType === 'operationButton') {\n const legacyLabel = legacyProp.renderConfig?.label;\n\n const match = operations.find(op => op.buttonLabel === legacyLabel);\n\n if (!match) {\n return true;\n }\n } else {\n const legacyName = legacyProp.name;\n\n const match = selected.find(prop => prop.name === legacyName);\n\n if (!match) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n private getFirstLinkableIndex(\n selectedProperties: AssetPropertyType[],\n config?: AssetTableWidgetConfig\n ): number {\n if (!selectedProperties?.length) return -1;\n\n if (config?.columnOrder?.length) {\n for (const orderItem of config.columnOrder) {\n if (orderItem.__origin !== 'selectedProperties') continue;\n\n const idx = selectedProperties.findIndex(\n prop => prop.name === orderItem.__id || prop.title === orderItem.__id\n );\n\n if (idx === -1) continue;\n\n const type = this.getColumnType(selectedProperties[idx]);\n\n // extract to a helper\n if (this.checkIfColumnIsLinkable(type)) {\n return idx;\n }\n }\n }\n\n return selectedProperties.findIndex(prop => {\n const type = this.getColumnType(prop);\n return this.checkIfColumnIsLinkable(type);\n });\n }\n\n private checkIfColumnIsLinkable(type: AssetColumnType): boolean {\n return type !== 'computed' && type !== 'alarm' && type !== 'date' && type !== 'icon';\n }\n\n private getColumnType(prop: AssetPropertyType): AssetColumnType {\n return prop.columnType\n ? prop.columnType\n : prop.name === 'c8y_ActiveAlarmsStatus'\n ? 'alarm'\n : prop.computed\n ? 'computed'\n : 'default';\n }\n\n private buildPropertyColumns(\n selectedProperties: AssetPropertyType[],\n firstLinkableIndex: number,\n config?: AssetTableWidgetConfig\n ): AssetTableExtendedColumn[] {\n return (selectedProperties || []).map((prop, index) => {\n const columnType = this.getColumnType(prop);\n\n const isLink =\n index === firstLinkableIndex && config?.showAsLink !== undefined\n ? config.showAsLink\n : false;\n\n const column = this.createPropertyColumn(prop, columnType, isLink, config);\n\n const uniqueName = prop.instanceId || prop.name || prop.title;\n if (prop.instanceId) {\n column.name = uniqueName;\n if (column.computedConfig) {\n column.computedConfig.__propertyName = prop.name;\n }\n }\n column.__origin = 'selectedProperties';\n column.__id = uniqueName;\n\n return column;\n });\n }\n\n private createPropertyColumn(\n prop: AssetPropertyType,\n columnType: AssetColumnType,\n isLink: boolean,\n config?: AssetTableWidgetConfig\n ): AssetTableExtendedColumn {\n switch (columnType) {\n case 'alarm':\n return new AlarmsDeviceGridColumnExtended({\n name: prop.name || prop.title,\n header: prop.columnLabel || prop.label || prop.title,\n visible: prop.visible ?? true\n });\n\n case 'date':\n return new DateAssetTableGridColumn({\n name: prop.name || prop.title,\n header: prop.columnLabel || prop.label || prop.title,\n visible: prop.visible ?? true,\n path: prop.name || prop.keyPath,\n sortOrder: prop.sortOrder ?? null\n });\n\n case 'icon':\n return new IconAssetTableGridColumn({\n name: prop.name || prop.title,\n path: prop.keyPath || prop.name,\n header: prop.columnLabel || prop.label || prop.title,\n iconConfig: prop.iconConfig || {},\n computedConfig: prop.computed && prop.config ? prop.config : null,\n showIconAndValue: config?.showIconAndValue,\n visible: prop.visible ?? true\n });\n\n case 'computed':\n return new ComputedAssetTableGridColumn({\n name: prop.name || prop.title,\n header: prop.columnLabel || prop.label || prop.title,\n computedConfig: prop.config || {},\n visible: prop.visible ?? true,\n sortOrder: prop.sortOrder ?? null\n });\n\n case 'default':\n default:\n return new DefaultAssetTableGridColumn({\n name: prop.name || prop.title,\n header: prop.columnLabel || prop.label || prop.title,\n custom: true,\n isLink,\n type: 'default',\n path: prop.keyPath || prop.name,\n visible: prop.visible ?? true,\n sortOrder: prop.sortOrder ?? null\n });\n }\n }\n\n private buildOperationColumns(\n operationColumns: ColumnConfigExtended[]\n ): AssetTableExtendedColumn[] {\n return (operationColumns || []).map(op => {\n const col = new OperationAssetTableGridColumn({\n name: op.buttonLabel,\n header: op.header,\n visible: op.visible ?? true,\n operationType: op.operationType,\n command: op.command,\n path: 'id'\n });\n\n (col as AssetTableExtendedColumn).__origin = 'operationColumns';\n (col as AssetTableExtendedColumn).__id = op.buttonLabel;\n\n return col;\n });\n }\n\n private applySortingAndOrdering(\n allColumns: AssetTableExtendedColumn[],\n config?: AssetTableWidgetConfig\n ): AssetTableExtendedColumn[] {\n // Apply sort orders\n if (config?.columnSortOrders) {\n for (const [columnName, sortOrder] of Object.entries(config.columnSortOrders)) {\n const col = allColumns.find(c => c.name === columnName);\n if (col) {\n col.sortOrder = sortOrder as 'asc' | 'desc';\n }\n }\n }\n\n // Apply column order\n if (!Array.isArray(config?.columnOrder) || config.columnOrder.length === 0) {\n return allColumns;\n }\n\n const ordered: AssetTableExtendedColumn[] = [];\n\n for (const orderItem of config.columnOrder) {\n const match = allColumns.find(\n c =>\n (c as AssetTableExtendedColumn).__id === orderItem.__id &&\n (c as AssetTableExtendedColumn).__origin === orderItem.__origin\n );\n\n if (match) {\n ordered.push(match);\n }\n }\n\n const missing = allColumns.filter(\n c =>\n !config.columnOrder.some(\n o =>\n o.__id === (c as AssetTableExtendedColumn).__id &&\n o.__origin === (c as AssetTableExtendedColumn).__origin\n )\n );\n\n return [...ordered, ...missing];\n }\n\n private buildAndFilterQuery(filterObj: Record<string, any>) {\n const andConditions: any[] = [];\n\n Object.entries(filterObj).forEach(([key, value]) => {\n if (Array.isArray(value)) {\n value.forEach(v => andConditions.push({ [key]: v }));\n } else if (typeof value === 'object' && value !== null) {\n // Convert { gt: '...' } → { __gt: '...' }\n const opEntries = Object.entries(value).map(([opKey, opValue]) => {\n const prefixedKey = opKey.startsWith('__') ? opKey : `__${opKey}`;\n return [prefixedKey, opValue];\n });\n\n const opObj = Object.fromEntries(opEntries);\n andConditions.push({ [key]: opObj });\n } else {\n andConditions.push({ [key]: value });\n }\n });\n\n return andConditions.length === 1 ? andConditions[0] : { __and: andConditions };\n }\n\n private extractFilters(obj: any, parentKey?: string, result: any = {}): Record<string, any> {\n Object.entries(obj).forEach(([key, value]) => {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n const dateOps = ['after', 'before', 'gt', 'lt', 'ge', 'le'];\n const hasDateOp = Object.keys(value).some(op => dateOps.includes(op));\n\n if (hasDateOp) {\n // Automatically add \".date\" for date filters\n const effectiveKey = parentKey ? `${parentKey}.date` : `${key}.date`;\n\n Object.entries(value).forEach(([opKey, opValue]) => {\n if (opValue != null && dateOps.includes(opKey)) {\n const op = opKey === 'after' ? 'gt' : opKey === 'before' ? 'lt' : opKey;\n\n result[effectiveKey] = {\n ...(result[effectiveKey] || {}),\n [op]: opValue\n };\n }\n });\n } else {\n const nextParent = parentKey ? `${parentKey}.${key}` : key;\n this.extractFilters(value, nextParent, result);\n }\n } else if (key === 'equals' && Array.isArray(value) && value.length > 0) {\n if (parentKey) {\n result[parentKey] = value.length === 1 ? value[0] : value;\n }\n }\n });\n\n return result;\n }\n\n /** Returns a query object defaultd on columns setup. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private getQueryObj(columns: Column[], defaultFilter = {}) {\n return transform(columns, (query, column) => this.addColumnQuery(query, column), {\n __filter: {},\n __orderby: [],\n ...defaultFilter\n });\n }\n\n /** Extends given query with a part defaultd on the setup of given column. */\n private addColumnQuery(query: AssetTableQuery, column: Column) {\n // when a column is marked as filterable\n if (column.filterable) {\n if (column.filterPredicate) {\n // so we use it as the expected value, * allow to search for it anywhere in the property\n query.__filter[column.path] = `*${column.filterPredicate}*`;\n }\n\n // in the case of custom filtering form, we're storing the query in `externalFilterQuery.query`\n if (column.externalFilterQuery) {\n const getFilter = column.filteringConfig.getFilter || identity;\n const queryObj = getFilter(column.externalFilterQuery);\n\n if (queryObj.__or) {\n query.__filter.__and = query.__filter.__and || [];\n query.__filter.__and.push(queryObj);\n } else if (queryObj.__and && get(query, '__filter.__and')) {\n queryObj.__and.map(obj => query.__filter.__and.push(obj));\n } else {\n assign(query.__filter, queryObj);\n }\n }\n }\n\n // when a column is sortable and has a specified sorting order\n if (column.sortable && column.sortOrder) {\n // add sorting condition for the configured column `path`\n query.__orderby.push({\n [column.path]: column.sortOrder === 'asc' ? 1 : -1\n });\n }\n\n return query;\n }\n\n /**\n * Lookup comparison object from COMPARISON_OPTIONS\n */\n private lookupComparison(\n comparison: ComparisonOption['value'] | ComparisonOption['sign']\n ): ComparisonOption {\n const allOptions = Object.values(COMPARISON_OPTIONS).flat();\n return allOptions.find(o => o.value === comparison || o.sign === comparison) ?? allOptions[1];\n }\n\n /**\n * Convert id + label + type into a c8y_JsonSchema format\n * Example:\n * id: 'c8ySchema!!lastMeasurement'\n * label: 'Last measurement'\n * type: 'string'\n * =>\n * {\n * properties: {\n * lastMeasurement: {\n * label: 'Last measurement',\n * type: 'string'\n * }\n * }\n * }\n */\n private convertIdToJsonSchema(legacyComputedProperty: any): any {\n const { id, label, type } = legacyComputedProperty;\n const parts = id.split('!!');\n const propertyName = parts.length > 1 ? parts[1] : id;\n\n return {\n properties: {\n [propertyName]: {\n label: label || propertyName,\n type: type || 'string'\n }\n }\n };\n }\n\n private async sortByComputedValues(\n data: IManagedObject[],\n sortedColumns: AssetTableExtendedColumn[]\n ): Promise<IManagedObject[]> {\n const valueCache = new Map<IManagedObject, Record<string, any>>();\n\n for (const item of data) {\n const values: Record<string, any> = {};\n for (const col of sortedColumns.filter(c => c.type === 'computed')) {\n const computedConfig = col.computedConfig;\n const propertyName = computedConfig?.__propertyName || col.name;\n try {\n const definition = await this.computedPropertiesService.getByName(propertyName);\n let