UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,135 lines (1,121 loc) 174 kB
import { NgTemplateOutlet, AsyncPipe, CommonModule as CommonModule$1, NgClass } from '@angular/common'; import * as i0 from '@angular/core'; import { Component, runInInjectionContext, inject, computed, ChangeDetectionStrategy, Injectable, EventEmitter, ViewChild, Output, Input } from '@angular/core'; import * as i2$2 from '@angular/forms'; import { FormGroup, FormBuilder, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; import * as i1$1 from '@c8y/client'; import { OperationService, InventoryService, QueriesUtil } from '@c8y/client'; import * as i1 from '@c8y/ngx-components'; import { DatePipe, BaseColumn, CustomColumn, IconDirective, AlertService, CellRendererContext, CommonModule, IntervalBasedReload, DEFAULT_INTERVAL_VALUES, WIDGET_TYPE_VALUES, CoreModule, CountdownIntervalComponent, ManagedObjectRealtimeService, WidgetGlobalAutoRefreshService, globalAutoRefreshLoading, WidgetActionWrapperComponent, ModalModule, SelectModule, DateTimePickerModule, C8yTranslateModule, BottomDrawerService, DropdownFocusTrapDirective, EmptyStateComponent, ListGroupModule, C8yTranslatePipe, HumanizePipe } from '@c8y/ngx-components'; import { AssetSelectorModule } from '@c8y/ngx-components/assets-navigator'; import { WidgetConfigService, ContextDashboardService } from '@c8y/ngx-components/context-dashboard'; import { from, switchMap, isObservable, of, map, firstValueFrom, tap, BehaviorSubject, Subject, merge, startWith, scan, distinctUntilChanged, shareReplay, takeUntil, skip } from 'rxjs'; import * as i2$1 from '@c8y/ngx-components/asset-properties'; import { AssetPropertySelectorDrawerComponent } from '@c8y/ngx-components/asset-properties'; import { transform, identity, get, assign } from 'lodash'; import * as i2 from '@c8y/ngx-components/device-grid'; import { AlarmsDeviceGridColumn } from '@c8y/ngx-components/device-grid'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1$2 from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core'; import { AssetTypeGridColumn } from '@c8y/ngx-components/data-grid-columns'; import * as i3 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { isEqual } from 'lodash-es'; import * as i3$1 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { RouterModule } from '@angular/router'; import { moveItemInArray, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop'; import * as i3$3 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule, BsDropdownDirective } from 'ngx-bootstrap/dropdown'; import * as i4 from '@ngx-formly/core'; import { FormlyModule } from '@ngx-formly/core'; import { BsModalService } from 'ngx-bootstrap/modal'; import * as i3$2 from '@c8y/ngx-components/icon-selector'; import { IconSelectorModule } from '@c8y/ngx-components/icon-selector'; import { OperationPickerService } from '@c8y/ngx-components/operation-picker'; class DateCellRendererComponent { constructor(context, columnUtilService) { this.context = context; this.columnUtilService = columnUtilService; this.isLink = false; this.href = null; } ngOnInit() { this.isLink = !!this.context?.property?.isLink; this.href = this.isLink ? this.columnUtilService.getHref(this.context.item) : null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DateCellRendererComponent, deps: [{ token: i1.CellRendererContext }, { token: i2.ColumnUtilService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: DateCellRendererComponent, isStandalone: true, selector: "c8y-date-cell-renderer", ngImport: i0, template: ` @if (isLink) { <a [href]="href"> {{ context.value | c8yDate }} </a> } @else { {{ context.value | c8yDate }} } `, isInline: true, dependencies: [{ kind: "pipe", type: DatePipe, name: "c8yDate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DateCellRendererComponent, decorators: [{ type: Component, args: [{ template: ` @if (isLink) { <a [href]="href"> {{ context.value | c8yDate }} </a> } @else { {{ context.value | c8yDate }} } `, selector: 'c8y-date-cell-renderer', imports: [DatePipe] }] }], ctorParameters: () => [{ type: i1.CellRendererContext }, { type: i2.ColumnUtilService }] }); const GLOBAL_INTERVAL_OPTION = 'global-interval'; const DEFAULT_INTERVAL_VALUE = 30_000; const COMPARISON_OPTIONS = { number: [ { label: gettext('Greater than'), value: 'GREATER_THAN', sign: '>' }, { label: gettext('Less than'), value: 'LESS_THAN', sign: '<' }, { label: gettext('Equal'), value: 'EQUAL', sign: '===' }, { label: gettext('Not equal'), value: 'NOT_EQUAL', sign: '!==' } ], date: [ { label: gettext('Before`date`'), value: 'BEFORE', sign: '<' }, { label: gettext('After`date`'), value: 'AFTER', sign: '>' }, { label: gettext('On`date`'), value: 'ON', sign: '===' } ], string: [ { label: gettext('Contains'), value: 'CONTAINS', sign: 'includes' }, { label: gettext('Equal'), value: 'EQUAL', sign: '===' }, { label: gettext('Not equal'), value: 'NOT_EQUAL', sign: '!==' }, { label: gettext('Starts with'), value: 'STARTS_WITH', sign: 'startsWith' }, { label: gettext('Ends with'), value: 'ENDS_WITH', sign: 'endsWith' } ], boolean: [ { label: gettext('Is true'), value: 'IS_TRUE', sign: 'true' }, { label: gettext('Is false'), value: 'IS_FALSE', sign: 'false' } ] }; class BaseColumnExtended extends BaseColumn { } class CustomColumnExtended extends CustomColumn { } class AlarmsDeviceGridColumnExtended extends AlarmsDeviceGridColumn { constructor(initialColumnConfig) { super(initialColumnConfig); } } class DateAssetTableGridColumn extends BaseColumnExtended { constructor(initialColumnConfig) { super(initialColumnConfig); this.path = this.path; this.name = this.name; this.header = this.header; this.type = 'date'; this.cellRendererComponent = DateCellRendererComponent; this.filterable = true; this.filteringConfig = { fields: [ { fieldGroup: [ { type: 'date-time', key: 'after', templateOptions: { label: gettext('From date') }, expressionProperties: { 'templateOptions.maxDate': (model) => model?.before } }, { type: 'date-time', key: 'before', templateOptions: { label: gettext('To date') }, expressionProperties: { 'templateOptions.minDate': (model) => model?.after } } ], validators: { atLeastOneFilled: { expression: (formGroup) => { const after = formGroup.get('after')?.value; const before = formGroup.get('before')?.value; return !!after || !!before; }, message: gettext('Specify at least one date.') } } } ], formGroup: new FormGroup({}), getFilter: model => { const filter = {}; const dates = model; if (dates && (dates.after || dates.before)) { filter.__and = []; if (dates.after) { const after = this.formatDate(dates.after); filter.__and.push({ [`${this.path}.date`]: { __gt: after } }); } if (dates.before) { const before = this.formatDate(dates.before); filter.__and.push({ [`${this.path}.date`]: { __lt: before } }); } } return filter; } }; this.sortable = true; this.sortingConfig = { pathSortingConfigs: [{ path: `${this.path}.date` }] }; } formatDate(dateToFormat) { return new Date(dateToFormat).toISOString(); } } class IconCellRendererComponent { constructor(context, computedPropertyService, columnUtilService) { this.context = context; this.computedPropertyService = computedPropertyService; this.columnUtilService = columnUtilService; // eslint-disable-next-line @typescript-eslint/no-explicit-any this.computed$ = null; } async ngOnInit() { if (this.context.property.computedConfig) { this.computed$ = this.getComputedValue(this.context.property, this.context.item); } } getComputedValue(property, context) { const propertyName = property.computedConfig?.__propertyName || property.name; return from(this.computedPropertyService.getByName(propertyName)).pipe(switchMap(definition => { let value = '-'; runInInjectionContext(definition.injector, () => { if (property.computedConfig?.dp?.length > 0) { property.computedConfig.dp[0].__target = { ...context }; } value = definition.value({ config: property.computedConfig, context, metadata: { mode: 'singleValue' } }); }); if (isObservable(value) || value instanceof Promise) { return value; } else { return of(value); } })); } matchedCondition(cellValue) { const iconConfigs = this.context?.property?.iconConfig; if (!Array.isArray(iconConfigs)) return null; return (iconConfigs.find(config => { const { comparison, value } = config; if (!comparison || value === undefined || value === null) return false; return this.evaluateCondition(cellValue, comparison.value, value); }) ?? null); } resolveValue(context) { const { item, property } = context; if (!item || !property) { return undefined; } const path = property.path; if (Array.isArray(path)) { return path.reduce((acc, key) => acc?.[key], item); } return item[path]; } evaluateCondition(cellValue, operator, value) { switch (operator) { case 'GREATER_THAN': return cellValue > value; case 'LESS_THAN': return cellValue < value; case 'EQUAL': return cellValue === value; case 'NOT_EQUAL': return cellValue !== value; case 'CONTAINS': return typeof cellValue === 'string' && cellValue.includes(value); case 'STARTS_WITH': return typeof cellValue === 'string' && cellValue.startsWith(value); case 'ENDS_WITH': return typeof cellValue === 'string' && cellValue.endsWith(value); case 'IS_TRUE': return cellValue === true; case 'IS_FALSE': return cellValue === false; case 'BEFORE': return new Date(cellValue) < new Date(value); case 'AFTER': return new Date(cellValue) > new Date(value); case 'ON': return (new Date(cellValue).toDateString() === new Date(value).toDateString()); default: return false; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: IconCellRendererComponent, deps: [{ token: i1.CellRendererContext }, { token: i2$1.ComputedPropertiesService }, { token: i2.ColumnUtilService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: IconCellRendererComponent, isStandalone: true, selector: "c8y-icon-cell-renderer", ngImport: i0, template: ` <ng-template #iconAndValueTemplate let-value> @if (matchedCondition(value); as match) { <i class="m-r-4" [c8yIcon]="match.icon" [style.color]="match.color"></i> @if (context.property.showIconAndValue) { <span class="text-truncate" [title]="value">{{ value }}</span> } } @else { <span class="text-truncate" [title]="value">{{ value }}</span> } </ng-template> @if (context?.property?.isLink) { <a class="d-flex a-i-center text-truncate" [href]="columnUtilService.getHref(context.item)"> @if (computed$ | async; as computed) { <ng-container *ngTemplateOutlet="iconAndValueTemplate; context: { $implicit: computed }" ></ng-container> } @else { <ng-container *ngTemplateOutlet=" iconAndValueTemplate; context: { $implicit: resolveValue(context) } " ></ng-container> } </a> } @else { <div class="d-flex a-i-center text-truncate"> @if (computed$ | async; as computed) { <ng-container *ngTemplateOutlet="iconAndValueTemplate; context: { $implicit: computed }" ></ng-container> } @else { <ng-container *ngTemplateOutlet=" iconAndValueTemplate; context: { $implicit: resolveValue(context) } " ></ng-container> } </div> }`, isInline: true, dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: IconCellRendererComponent, decorators: [{ type: Component, args: [{ template: ` <ng-template #iconAndValueTemplate let-value> @if (matchedCondition(value); as match) { <i class="m-r-4" [c8yIcon]="match.icon" [style.color]="match.color"></i> @if (context.property.showIconAndValue) { <span class="text-truncate" [title]="value">{{ value }}</span> } } @else { <span class="text-truncate" [title]="value">{{ value }}</span> } </ng-template> @if (context?.property?.isLink) { <a class="d-flex a-i-center text-truncate" [href]="columnUtilService.getHref(context.item)"> @if (computed$ | async; as computed) { <ng-container *ngTemplateOutlet="iconAndValueTemplate; context: { $implicit: computed }" ></ng-container> } @else { <ng-container *ngTemplateOutlet=" iconAndValueTemplate; context: { $implicit: resolveValue(context) } " ></ng-container> } </a> } @else { <div class="d-flex a-i-center text-truncate"> @if (computed$ | async; as computed) { <ng-container *ngTemplateOutlet="iconAndValueTemplate; context: { $implicit: computed }" ></ng-container> } @else { <ng-container *ngTemplateOutlet=" iconAndValueTemplate; context: { $implicit: resolveValue(context) } " ></ng-container> } </div> }`, selector: 'c8y-icon-cell-renderer', imports: [AsyncPipe, IconDirective, NgTemplateOutlet] }] }], ctorParameters: () => [{ type: i1.CellRendererContext }, { type: i2$1.ComputedPropertiesService }, { type: i2.ColumnUtilService }] }); class IconAssetTableGridColumn extends BaseColumnExtended { constructor(initialColumnConfig) { super(initialColumnConfig); this.path = this.path; this.name = this.name; this.header = this.header; this.computedConfig = this.computedConfig; this.type = 'icon'; this.cellRendererComponent = IconCellRendererComponent; this.iconConfig = this.iconConfig; this.showIconAndValue = this.showIconAndValue; this.filterable = false; this.sortable = false; } } class OperationCellRendererComponent { constructor() { this.operations = inject(OperationService); this.alertService = inject(AlertService); this.inventoryService = inject(InventoryService); this.translateService = inject(TranslateService); this.context = inject(CellRendererContext); this.shouldShowButton = computed(() => { const operationType = this.context.property?.operationType; const device = this.context.item; if (operationType !== 'maintenance') { return true; } return this.canSwitchResponseInterval(device); }, ...(ngDevMode ? [{ debugName: "shouldShowButton" }] : [])); } async onOperationClick() { const operationType = this.context.property?.operationType; const device = this.context.item; try { if (operationType === 'maintenance') { let payload; if (this.canSwitchResponseInterval(device)) { payload = { id: device.id, c8y_RequiredAvailability: { responseInterval: -device.c8y_RequiredAvailability.responseInterval } }; } else { payload = { id: device.id, c8y_RequiredAvailability: null }; } await this.inventoryService.update(payload); this.alertService.success(gettext('Maintenance mode toggled.')); } else { const deviceId = this.context.value; const commandBody = typeof this.context.property.command === 'string' ? JSON.parse(this.context.property.command) : this.context.property.command; const body = { deviceId, ...commandBody }; await this.operations.create(body); this.alertService.success(gettext('Operation created.')); } } catch (error) { this.alertService.danger(this.translateService.instant(gettext('Failed to execute operation: "{{ errorMessage }}"'), { errorMessage: this.translateService.instant(error.message) })); } } canSwitchResponseInterval(device) { return (device && device.c8y_RequiredAvailability && device.c8y_RequiredAvailability.responseInterval && parseInt(device.c8y_RequiredAvailability.responseInterval, 10) !== 0); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationCellRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: OperationCellRendererComponent, isStandalone: true, selector: "c8y-operation-cell-renderer", ngImport: i0, template: ` @if (shouldShowButton()) { <button class="btn btn-primary btn-sm" type="button" (click)="onOperationClick()"> {{ context.property?.name ?? '' | translate }} </button> } @else { <span class="text-muted">-</span> } `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-operation-cell-renderer', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (shouldShowButton()) { <button class="btn btn-primary btn-sm" type="button" (click)="onOperationClick()"> {{ context.property?.name ?? '' | translate }} </button> } @else { <span class="text-muted">-</span> } `, imports: [CommonModule] }] }] }); class OperationAssetTableGridColumn extends BaseColumnExtended { constructor(initialColumnConfig) { super(initialColumnConfig); this.path = this.path; this.name = this.name; this.header = this.header || gettext('Operation'); this.type = 'operation'; this.cellRendererComponent = OperationCellRendererComponent; this.command = this.command; this.isOperation = true; this.operationType = this.operationType || 'operation'; this.filterable = false; this.sortable = false; } } class ComputedCellRendererComponent { constructor(context, computedPropertyService) { this.context = context; this.computedPropertyService = computedPropertyService; } ngOnInit() { this.computedValue = this.getCallbackComputedPropertyValue(this.context.property, this.context.item).pipe(map(value => (typeof value === 'object' && value !== null ? JSON.stringify(value) : value))); } getCallbackComputedPropertyValue(property, context) { const propertyName = property.computedConfig?.__propertyName || property.name; return from(this.computedPropertyService.getByName(propertyName)).pipe(switchMap(definition => { let value = '-'; runInInjectionContext(definition.injector, () => { if (this.context?.property.computedConfig?.dp?.length > 0) { this.context.property.computedConfig.dp[0].__target = { ...context }; } value = definition.value({ config: this.context?.property.computedConfig, context, metadata: { mode: 'singleValue' } }); }); if (isObservable(value) || value instanceof Promise) { return value; } else { return of(value); } })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ComputedCellRendererComponent, deps: [{ token: i1.CellRendererContext }, { token: i2$1.ComputedPropertiesService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: ComputedCellRendererComponent, isStandalone: true, selector: "c8y-computed-cell-renderer", ngImport: i0, template: `{{ computedValue | async }}`, isInline: true, dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ComputedCellRendererComponent, decorators: [{ type: Component, args: [{ template: `{{ computedValue | async }}`, selector: 'c8y-computed-cell-renderer', imports: [AsyncPipe] }] }], ctorParameters: () => [{ type: i1.CellRendererContext }, { type: i2$1.ComputedPropertiesService }] }); class ComputedAssetTableGridColumn extends BaseColumnExtended { constructor(initialColumnConfig) { super(initialColumnConfig); this.name = this.name; this.header = this.header; this.computedConfig = this.computedConfig; this.visible = this.visible; this.type = 'computed'; this.cellRendererComponent = ComputedCellRendererComponent; this.filterable = false; this.sortable = true; } } class DefaultCellRendererComponent { constructor(context, columnUtilService) { this.context = context; this.columnUtilService = columnUtilService; this.isLink = false; this.href = null; this.displayValue = null; } ngOnInit() { this.isLink = !!this.context?.property?.isLink; this.href = this.isLink ? this.columnUtilService.getHref(this.context.item) : null; this.displayValue = this.formatValue(this.context.value); } formatValue(value) { if (typeof value === 'object') { return JSON.stringify(value); } return String(value); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DefaultCellRendererComponent, deps: [{ token: i1.CellRendererContext }, { token: i2.ColumnUtilService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: DefaultCellRendererComponent, isStandalone: true, selector: "c8y-text-cell-renderer", ngImport: i0, template: ` @if (isLink) { <a [href]="href">{{ displayValue }}</a> } @else { {{ displayValue }} } `, isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DefaultCellRendererComponent, decorators: [{ type: Component, args: [{ template: ` @if (isLink) { <a [href]="href">{{ displayValue }}</a> } @else { {{ displayValue }} } `, selector: 'c8y-text-cell-renderer' }] }], ctorParameters: () => [{ type: i1.CellRendererContext }, { type: i2.ColumnUtilService }] }); class DefaultAssetTableGridColumn extends CustomColumnExtended { constructor(initialColumnConfig) { super(initialColumnConfig); this.type = this.type || 'default'; this.isLink = this.isLink || false; this.cellRendererComponent = DefaultCellRendererComponent; this.filterable = true; this.sortable = true; } } class AssetTableService { constructor(inventoryService, computedPropertiesService, injector) { this.inventoryService = inventoryService; this.computedPropertiesService = computedPropertiesService; this.injector = injector; this.queriesUtil = new QueriesUtil(); } getColumns(selectedProperties, operationColumns, config) { const firstLinkableIndex = this.getFirstLinkableIndex(selectedProperties, config); const propertyColumns = this.buildPropertyColumns(selectedProperties, firstLinkableIndex, config); const opColumns = this.buildOperationColumns(operationColumns); let allColumns = [...propertyColumns, ...opColumns]; if (config?.showStatusIcon) { const statusCol = new AssetTypeGridColumn(); statusCol.name = '__status'; statusCol.__origin = 'status'; statusCol.__id = 'status'; statusCol.header = gettext('Status'); allColumns = [statusCol, ...allColumns]; } return this.applySortingAndOrdering(allColumns, config); } buildAssetQueryObj(config) { const assetFilter = config.device ? [ { __or: [ config.includeDescendants ? { __isinhierarchyof: config.device.id } : config.isDeviceAssetSelected ? { id: config.device.id } : { __bygroupid: config.device.id } ] } ] : []; return { __and: [ ...assetFilter, { __or: [ { __has: 'c8y_IsDevice' }, { __has: 'c8y_IsAsset' }, { __has: 'c8y_IsDeviceGroup' } ] } ] }; } async getAssets(config, columns, preConfiguredFilter = {}, pagination = { pageSize: 100 }) { const queryObj = this.buildAssetQueryObj(config); const columnQueryObj = this.getQueryObj(columns); const preConfigFilterObj = this.extractFilters(preConfiguredFilter); const filterQuery = this.buildAndFilterQuery(preConfigFilterObj); const andConditions = [queryObj]; if (Object.keys(columnQueryObj.__filter).length > 0) { andConditions.push({ __filter: columnQueryObj.__filter }); } if (Object.keys(preConfigFilterObj).length > 0) { andConditions.push({ __filter: filterQuery }); } const combinedQueryObj = { __filter: { __and: andConditions }, __orderby: columnQueryObj.__orderby }; const query = this.queriesUtil.buildQuery(combinedQueryObj); const result = await this.inventoryService.list({ query, pageSize: pagination.pageSize, currentPage: pagination.currentPage || 1, withTotalElements: true, withTotalPages: true }); const allSortedColumns = columns.filter((col) => col.sortable && col.sortOrder); const hasComputedSort = allSortedColumns.some(col => col.type === 'computed'); if (hasComputedSort && result.data.length > 1) { result.data = await this.sortByComputedValues(result.data, allSortedColumns); } return result; } async migrateLegacyProperties(config) { if (!config?.options?.properties) { return config; } if (config.device) { config.device = { ...(await this.inventoryService.detail(config.device.id)).data }; config.isDeviceAssetSelected = 'c8y_IsDevice' in config.device; } const selectedProperties = []; const operationColumns = []; for (const prop of config.options.properties) { const isOperation = prop?.renderType === 'operationButton' || prop?.isAction; if (isOperation) { operationColumns.push({ header: prop.label, operationType: prop.renderConfig?.actionType === 'toggleMaintenanceMode' ? 'maintenance' : 'operation', operation: null, command: JSON.stringify(prop?.config?.args?.[0] ?? { description: 'Command description', c8y_Command: { text: '<command>' } }, null, 2), buttonLabel: prop.renderConfig?.label ?? prop.label }); continue; } // Map old renderConfig.map -> iconConfig using proper comparison if (prop.renderConfig?.map && Array.isArray(prop.renderConfig.map) && prop.renderType === 'iconMap') { prop.columnType = 'icon'; prop.name = prop.name || prop.keyPath?.[0] || prop.label; prop.iconConfig = prop.renderConfig.map.map((mapItem) => { const comparisonObj = this.lookupComparison(mapItem.comparison); return { comparison: comparisonObj, color: mapItem.color, icon: mapItem.icon, value: mapItem.value }; }); } // preserve computed properties and build c8y_JsonSchema if (prop.computed) { prop.c8y_JsonSchema = this.convertIdToJsonSchema(prop); const parts = prop.id.split('!!'); const propertyName = parts.length > 1 ? parts[1] : prop.id; prop.name = propertyName; // Use legacy config._id as instanceId so multiple computed columns sharing the // same base name (e.g. two "lastMeasurement" data-point columns) are kept distinct. if (prop.config?._id) { prop.instanceId = prop.config._id; } } prop.active = prop.__active; // last keypath segment is used as name if name is not provided prop.name = prop.name || prop.keyPath?.[prop.keyPath.length - 1] || prop.label; selectedProperties.push(prop); } return { ...config, selectedProperties, operationColumns, refreshInterval: config.refreshInterval ?? 30000, refreshOption: config.refreshOption ?? 'interval', includeDescendants: config.includeDescendants ?? false, isDeviceAssetSelected: config.isDeviceAssetSelected ?? true, widgetInstanceGlobalAutoRefreshContext: config.widgetInstanceGlobalAutoRefreshContext ?? null, widgetInstanceGlobalTimeContext: config.widgetInstanceGlobalTimeContext ?? false, showAsLink: config.showAsLink ?? true, showStatusIcon: config.showStatusIcon ?? true, displayOptions: { ...config.displayOptions, gridHeader: false, footer: false } }; } migrateToLegacyProperties(config) { if (config.options) { delete config.options.properties; } const properties = []; // Convert selectedProperties back to legacy format for (const prop of config.selectedProperties || []) { const legacyProp = { ...prop }; // Restore computed property fields if (prop.computed) { legacyProp.__active = prop.active; legacyProp.id = prop.name ? `c8ySchema!!${prop.name}` : prop.id; legacyProp.label = prop.header || prop.label || prop.name; legacyProp.type = prop.c8y_JsonSchema?.properties?.[prop.name]?.type || 'string'; legacyProp.computed = true; } // Restore iconMap if (prop.columnType === 'icon' && Array.isArray(prop.iconConfig)) { legacyProp.renderType = 'iconMap'; legacyProp.renderConfig = { map: prop.iconConfig.map((icon) => ({ comparison: icon.comparison?.value ?? icon.comparison, color: icon.color, icon: icon.icon, value: icon.value })) }; } // Restore alarm type if (prop.columnType === 'alarm') { legacyProp.renderType = 'alarm'; } // Restore default type if (!prop.columnType || prop.columnType === 'default' || prop.columnType === 'base') { legacyProp.renderType = 'default'; } legacyProp.id = 'c8ySchema!!' + prop.name; legacyProp.__active = prop.active; properties.push(legacyProp); } // Convert operationColumns back to legacy format for (const op of config.operationColumns || []) { let commandObj; if (typeof op.command === 'string') { try { commandObj = JSON.parse(op.command); } catch { commandObj = op.command; // fallback if not valid JSON } } else { commandObj = op.command; } properties.push({ label: op.header, renderType: 'operationButton', isAction: true, renderConfig: { args: Array.isArray(commandObj) ? commandObj : [commandObj], label: op.buttonLabel, actionType: op.operationType === 'maintenance' ? 'toggleMaintenanceMode' : 'createOperation', deviceTypes: ['c8y_DeviceGroup', 'c8y_MQTTDevice', 'c8y_DeviceSubgroup'] }, keyPath: [op.operationType ? 'createOperation' : 'toggleMaintenanceMode'], __active: true }); } // Build legacy config object return { ...config, options: { ...config.options, properties } }; } isMigrationNeeded(config) { const legacyProps = config?.options?.properties; if (!legacyProps?.length) { return false; } if (!config.selectedProperties || !config.operationColumns) { return true; } const selected = config.selectedProperties ?? []; const operations = config.operationColumns ?? []; if (legacyProps.length !== selected.length + operations.length) { return true; } for (const legacyProp of legacyProps) { if (legacyProp.renderType === 'operationButton') { const legacyLabel = legacyProp.renderConfig?.label; const match = operations.find(op => op.buttonLabel === legacyLabel); if (!match) { return true; } } else { const legacyName = legacyProp.name; const match = selected.find(prop => prop.name === legacyName); if (!match) { return true; } } } return false; } getFirstLinkableIndex(selectedProperties, config) { if (!selectedProperties?.length) return -1; if (config?.columnOrder?.length) { for (const orderItem of config.columnOrder) { if (orderItem.__origin !== 'selectedProperties') continue; const idx = selectedProperties.findIndex(prop => prop.name === orderItem.__id || prop.title === orderItem.__id); if (idx === -1) continue; const type = this.getColumnType(selectedProperties[idx]); // extract to a helper if (this.checkIfColumnIsLinkable(type)) { return idx; } } } return selectedProperties.findIndex(prop => { const type = this.getColumnType(prop); return this.checkIfColumnIsLinkable(type); }); } checkIfColumnIsLinkable(type) { return type !== 'computed' && type !== 'alarm' && type !== 'date' && type !== 'icon'; } getColumnType(prop) { return prop.columnType ? prop.columnType : prop.name === 'c8y_ActiveAlarmsStatus' ? 'alarm' : prop.computed ? 'computed' : 'default'; } buildPropertyColumns(selectedProperties, firstLinkableIndex, config) { return (selectedProperties || []).map((prop, index) => { const columnType = this.getColumnType(prop); const isLink = index === firstLinkableIndex && config?.showAsLink !== undefined ? config.showAsLink : false; const column = this.createPropertyColumn(prop, columnType, isLink, config); const uniqueName = prop.instanceId || prop.name || prop.title; if (prop.instanceId) { column.name = uniqueName; if (column.computedConfig) { column.computedConfig.__propertyName = prop.name; } } column.__origin = 'selectedProperties'; column.__id = uniqueName; return column; }); } createPropertyColumn(prop, columnType, isLink, config) { switch (columnType) { case 'alarm': return new AlarmsDeviceGridColumnExtended({ name: prop.name || prop.title, header: prop.columnLabel || prop.label || prop.title, visible: prop.visible ?? true }); case 'date': return new DateAssetTableGridColumn({ name: prop.name || prop.title, header: prop.columnLabel || prop.label || prop.title, visible: prop.visible ?? true, path: prop.name || prop.keyPath, sortOrder: prop.sortOrder ?? null }); case 'icon': return new IconAssetTableGridColumn({ name: prop.name || prop.title, path: prop.keyPath || prop.name, header: prop.columnLabel || prop.label || prop.title, iconConfig: prop.iconConfig || {}, computedConfig: prop.computed && prop.config ? prop.config : null, showIconAndValue: config?.showIconAndValue, visible: prop.visible ?? true }); case 'computed': return new ComputedAssetTableGridColumn({ name: prop.name || prop.title, header: prop.columnLabel || prop.label || prop.title, computedConfig: prop.config || {}, visible: prop.visible ?? true, sortOrder: prop.sortOrder ?? null }); case 'default': default: return new DefaultAssetTableGridColumn({ name: prop.name || prop.title, header: prop.columnLabel || prop.label || prop.title, custom: true, isLink, type: 'default', path: prop.keyPath || prop.name, visible: prop.visible ?? true, sortOrder: prop.sortOrder ?? null }); } } buildOperationColumns(operationColumns) { return (operationColumns || []).map(op => { const col = new OperationAssetTableGridColumn({ name: op.buttonLabel, header: op.header, visible: op.visible ?? true, operationType: op.operationType, command: op.command, path: 'id' }); col.__origin = 'operationColumns'; col.__id = op.buttonLabel; return col; }); } applySortingAndOrdering(allColumns, config) { // Apply sort orders if (config?.columnSortOrders) { for (const [columnName, sortOrder] of Object.entries(config.columnSortOrders)) { const col = allColumns.find(c => c.name === columnName); if (col) { col.sortOrder = sortOrder; } } } // Apply column order if (!Array.isArray(config?.columnOrder) || config.columnOrder.length === 0) { return allColumns; } const ordered = []; for (const orderItem of config.columnOrder) { const match = allColumns.find(c => c.__id === orderItem.__id && c.__origin === orderItem.__origin); if (match) { ordered.push(match); } } const missing = allColumns.filter(c => !config.columnOrder.some(o => o.__id === c.__id && o.__origin === c.__origin)); return [...ordered, ...missing]; } buildAndFilterQuery(filterObj) { const andConditions = []; Object.entries(filterObj).forEach(([key, value]) => { if (Array.isArray(value)) { value.forEach(v => andConditions.push({ [key]: v })); } else if (typeof value === 'object' && value !== null) { // Convert { gt: '...' } → { __gt: '...' } const opEntries = Object.entries(value).map(([opKey, opValue]) => { const prefixedKey = opKey.startsWith('__') ? opKey : `__${opKey}`; return [prefixedKey, opValue]; }); const opObj = Object.fromEntries(opEntries); andConditions.push({ [key]: opObj }); } else { andConditions.push({ [key]: value }); } }); return andConditions.length === 1 ? andConditions[0] : { __and: andConditions }; } extractFilters(obj, parentKey, result = {}) { Object.entries(obj).forEach(([key, value]) => { if (value && typeof value === 'object' && !Array.isArray(value)) { const dateOps = ['after', 'before', 'gt', 'lt', 'ge', 'le']; const hasDateOp = Object.keys(value).some(op => dateOps.includes(op)); if (hasDateOp) { // Automatically add ".date" for date filters const effectiveKey = parentKey ? `${parentKey}.date` : `${key}.date`; Object.entries(value).forEach(([opKey, opValue]) => { if (opValue != null && dateOps.includes(opKey)) { const op = opKey === 'after' ? 'gt' : opKey === 'before' ? 'lt' : opKey; result[effectiveKey] = { ...(result[effectiveKey] || {}), [op]: opValue }; } }); } else { const nextParent = parentKey ? `${parentKey}.${key}` : key; this.extractFilters(value, nextParent, result); } } else if (key === 'equals' && Array.isArray(value) && value.length > 0) { if (parentKey) { result[parentKey] = value.length === 1 ? value[0] : value; } } }); return result; } /** Returns a query object defaultd on columns setup. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any getQueryObj(columns, defaultFilter = {}) { return transform(columns, (query, column) => this.addColumnQuery(query, column), { __filter: {}, __orderby: [], ...defaultFilter }); } /** Extends given query with a part defaultd on the setup of given column. */ addColumnQuery(query, column) { // when a column is marked as filterable if (column.filterable) { if (column.filterPredicate) { // so we use it as the expected value, * allow to search for it anywhere in the property query.__filter[column.path] = `*${column.filterPredicate}*`; } // in the case of custom filtering form, we're storing the query in `externalFilterQuery.query` if (column.externalFilterQuery) { const getFilter = column.filteringConfig.getFilter || identity; const queryObj = getFilter(column.externalFilterQuery); if (queryObj.__or) { query.__filter.__and = query.__filter.__and || []; query.__filter.__and.push(queryObj); } else if (queryObj.__and && get(query, '__filter.__and')) { queryObj.__and.map(obj => query.__filter.__and.push(obj)); } else { assign(query.__filter, queryObj); } } } // when a column is sortable and has a specified sorting order if (column.sortable && column.sortOrder) { // add sorting condition for the configured column `path` query.__orderby.push({ [column.path]: column.sortOrder === 'asc' ? 1 : -1 }); } return query; } /** * Lookup comparison object from COMPARISON_OPTIONS */ lookupComparison(comparison) { const allOptions = Object.values(COMPARISON_OPTIONS).flat(); return allOptions.find(o => o.value === comparison || o.sign === comparison) ?? allOptions[1]; } /** * Convert id + label + type into a c8y_JsonSchema format * Example: * id: 'c8ySchema!!lastMeasurement' * label: 'Last measurement' * type: 'string' * => * { * properties: { * lastMeasurement: { * label: 'Last measurement', * type: 'string' * } * } * } */ convertIdToJsonSchema(legacyComputedProperty) { const { id, label, type } = legac