@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,135 lines (1,121 loc) • 174 kB
JavaScript
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