@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
381 lines (377 loc) • 67.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, inject, Input, Component, Injector, runInInjectionContext, signal, ViewChild } from '@angular/core';
import { DashboardDetailService } from '@c8y/ngx-components/context-dashboard';
import * as i4$1 from 'ngx-bootstrap/popover';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { gettext } from '@c8y/ngx-components/gettext';
import * as i1 from '@c8y/ngx-components';
import { CoreModule, DynamicComponentService, AlertService, CommonModule, FormsModule } from '@c8y/ngx-components';
import { EditorComponent } from '@c8y/ngx-components/editor';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { Subject, debounceTime, takeUntil } from 'rxjs';
import * as i3$1 from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { cloneDeep } from 'lodash-es';
import { saveAs } from 'file-saver';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import * as i3 from '@c8y/ngx-components/assets-navigator';
import { AssetSelectorModule } from '@c8y/ngx-components/assets-navigator';
import * as i4 from 'ngx-bootstrap/dropdown';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import * as i5 from 'ngx-bootstrap/tooltip';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import * as i2 from '@angular/common';
class JsonValidationService {
constructor(dynamicComponentService) {
this.dynamicComponentService = dynamicComponentService;
this.OMIT_ERRORS = ['must NOT be valid', 'must match exactly one schema in oneOf'];
this.ajv = new Ajv({ verbose: true, allErrors: true });
addFormats(this.ajv);
}
async validateDashboard(dashboardJson, schemas) {
try {
// Parse JSON string to object
const dashboard = JSON.parse(dashboardJson);
// Get or use provided schemas
const validationSchemas = schemas || (await this.getJsonSchemas(dashboard));
// Create a new Ajv instance
const validator = new Ajv({
verbose: true,
allErrors: true
});
addFormats(validator);
// Add all component schemas individually
validationSchemas.forEach(({ uri, schema }) => {
validator.addSchema(schema, uri);
});
const errorsMap = new Map();
// Validate main dashboard structure first (without children)
const mainSchema = validationSchemas.find(s => s.uri === 'context-dashboard.c8y-schema-loader')?.schema;
if (!mainSchema) {
throw new Error('Dashboard schema not found');
}
// Create a copy without children for main validation
const dashboardWithoutChildren = { ...dashboard };
delete dashboardWithoutChildren.children;
const mainValidate = validator.compile(mainSchema);
mainValidate(dashboardWithoutChildren);
// Now validate each widget with its specific schema
if (dashboard.children) {
Object.entries(dashboard.children).forEach(([widgetId, widget]) => {
const componentId = widget.componentId;
// Find the correct schema for this widget type
const widgetSchema = validationSchemas.find(s => s.uri === componentId)?.schema;
if (widgetSchema) {
// Validate this widget's config against its specific schema
const validateWidgetConfig = validator.compile(widgetSchema);
const isValid = validateWidgetConfig(widget.config);
if (!isValid && validateWidgetConfig.errors) {
validateWidgetConfig.errors.forEach(error => {
const message = error.message || 'Unknown error';
const basePath = `/children/${widgetId}/config`;
const relativePath = error.instancePath;
const fullPath = basePath + relativePath;
if (!this.OMIT_ERRORS.includes(message)) {
errorsMap.set(`${message}::${fullPath}`, {
message,
path: fullPath
});
}
});
}
}
});
}
// Process any errors from main validation
if (mainValidate.errors) {
mainValidate.errors.forEach(error => {
const message = error.message || 'Unknown error';
const path = error.instancePath;
// Skip children validation errors as we handled them separately
if (!path.includes('/children/') && !this.OMIT_ERRORS.includes(message)) {
errorsMap.set(`${message}::${path}`, { message, path });
}
});
}
return Array.from(errorsMap.values());
}
catch (error) {
if (error instanceof SyntaxError) {
return [{ message: 'Invalid JSON syntax', path: '' }];
}
throw error;
}
}
/**
* Returns JSON schema for dashboard and all its children.
* Children are referenced by their componentId. Dashboard schema is enriched with references to children schemas.
* @param dashboard Context dashboard object.
*/
async getJsonSchemas(dashboard) {
try {
const dashboardChildrenIds = [
...new Set(Object.entries(dashboard.children || {}).map(([_, value]) => value.componentId))
];
const widgetSchemasPromises = dashboardChildrenIds.map(id => this.getSchemaForWidget(id));
const widgetSchemasResults = await Promise.allSettled(widgetSchemasPromises);
// Filter out failed or null schemas
const validWidgetSchemas = widgetSchemasResults
.filter((result) => result.status === 'fulfilled' && result.value !== null)
.map(result => result.value);
const { schema: dashboardSchemaOriginal } = await import('c8y-schema-loader?interfaceName=ContextDashboard!@c8y/ngx-components/context-dashboard');
const dashboardSchema = JSON.parse(JSON.stringify(dashboardSchemaOriginal));
// Modify Widget definition with proper schema structure
if (validWidgetSchemas.length > 0) {
// Remove any existing allOf or oneOf conditions that might interfere
if (dashboardSchema.definitions.Widget.allOf) {
delete dashboardSchema.definitions.Widget.allOf;
}
// Set up the oneOf condition
dashboardSchema.definitions.Widget.oneOf = this.getWidgetSchemasReferences(validWidgetSchemas.map(({ id }) => id));
}
return [
{
uri: 'context-dashboard.c8y-schema-loader',
fileMatch: ['*'],
schema: dashboardSchema
},
...validWidgetSchemas.map(({ id, schema }) => ({
uri: id,
schema: schema
}))
];
}
catch (error) {
console.error('Error processing dashboard schemas:', error);
throw error;
}
}
async getSchemaForWidget(id) {
try {
const def = await this.dynamicComponentService.getById(id);
if (def?.data?.schema && typeof def.data.schema === 'function') {
const { schema } = await def.data.schema();
return { id, schema };
}
return null;
}
catch (error) {
console.warn(`Failed to fetch schema for widget ${id}:`, error);
return null;
}
}
getWidgetSchemasReferences(widgetIds) {
// provide references to all children schemas
const schemasPart = widgetIds.map(id => {
return {
properties: {
componentId: { const: id },
config: { $ref: `${id}#` } // Ensure proper URI reference format
},
required: ['componentId'] // Only require componentId
};
});
// provide default schema for object if component has no schema definition
const defaultPart = {
not: {
properties: {
componentId: {
enum: widgetIds
}
}
},
properties: {
config: {
type: 'object',
additionalProperties: true
}
}
};
return [...schemasPart, defaultPart];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: JsonValidationService, deps: [{ token: i1.DynamicComponentService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: JsonValidationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: JsonValidationService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.DynamicComponentService }] });
class AssignWidgetAssetModalComponent {
constructor() {
this.notSupportedWidgets = [];
this.modalTitle = gettext(`
For each asset in the supported widgets, the suggested source is based on the original one.
You can either accept the suggestion, select a different asset, or decide to configure it later.
Please note: If you don't pick any of the options available, the widget will not be imported.
`);
this.assetIdInfo = gettext('id: {{ assetId }}');
this.acceptSuggestedLabel = gettext('Accept suggested asset');
this.noSuggestedAssetLabel = gettext('No suggestion available');
this.updatedWidgetAssets = [];
this.result = new Promise((resolve, reject) => {
this._close = resolve;
this._cancel = reject;
});
this.bsModalRef = inject(BsModalRef);
}
ngOnInit() {
this.updatedWidgetAssets = this.assetsToAlign.map(asset => ({
...asset
}));
this.hasSuggestedAssets = this.updatedWidgetAssets.some(asset => !!asset.deviceRef.value.suggestedDevice);
}
apply() {
const alignedAssets = this.updatedWidgetAssets.map(asset => {
return {
widgetId: asset.widgetId,
title: asset.title,
deviceRef: {
path: asset.deviceRef.path,
value: asset.selectedAsset == null
? asset.selectedAsset
: { ...asset.selectedAsset, suggestedDevice: null }
}
};
});
this._close(alignedAssets);
this.bsModalRef.hide();
}
acceptAllSuggested() {
this.updatedWidgetAssets.forEach(asset => {
asset.selectedAsset = asset.deviceRef.value.suggestedDevice;
});
}
acceptAllSuggestedAndApply() {
this.acceptAllSuggested();
this.apply();
}
cancel() {
this.bsModalRef.hide();
this._cancel();
}
selectionChange($event, widgetAsset) {
widgetAsset.selectedAsset = $event.change.item;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssignWidgetAssetModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: AssignWidgetAssetModalComponent, isStandalone: true, selector: "c8y-assign-widget-asset-modal", inputs: { assetsToAlign: "assetsToAlign", contextAsset: "contextAsset", notSupportedWidgets: "notSupportedWidgets" }, ngImport: i0, template: "<div class=\"viewport-modal has-asset-selector\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'th-large'\"></i>\n <div\n class=\"modal-title\"\n id=\"modal-title\"\n translate\n >\n Select and confirm widget assets mapping\n </div>\n </div>\n <div\n class=\"inner-scroll\"\n id=\"modal-body\"\n >\n <div class=\"p-16\">\n <p class=\"text-center text-balance\">\n {{ modalTitle | translate }}\n </p>\n </div>\n <div\n class=\"d-flex fit-w j-c-center p-16 p-t-0\"\n *ngIf=\"notSupportedWidgets.length\"\n >\n <div class=\"alert alert-warning\">\n {{\n 'Widgets not supported for asset mapping: {{ notSupportedWidgets\n\n\n\n }}' | translate: { notSupportedWidgets: notSupportedWidgets.join(', ') } }}\n </div>\n </div>\n <div\n class=\"p-16 d-flex j-c-center\"\n *ngIf=\"!assetsToAlign.length\"\n >\n <c8y-ui-empty-state\n class=\"\"\n [icon]=\"'empty-box'\"\n [title]=\"'No assets to assign' | translate\"\n ></c8y-ui-empty-state>\n </div>\n <div *ngIf=\"assetsToAlign.length\">\n <c8y-list-group class=\"separator-top no-border-last\">\n <c8y-li class=\"sticky-top bg-level-1 hidden-sm hidden-xs\">\n <c8y-li-icon></c8y-li-icon>\n <div class=\"d-flex\">\n <div class=\"col-md-2\">\n <!-- title -->\n <p\n class=\"text-truncate text-medium\"\n translate\n >\n Widget\n </p>\n </div>\n <div class=\"col-md-10\">\n <div class=\"row\">\n <div class=\"col-md-4 p-r-0\">\n <p\n class=\"text-medium\"\n translate\n >\n Original source\n </p>\n </div>\n <div class=\"col-md-1\">\n <button\n class=\"btn btn-default btn-sm\"\n [attr.aria-label]=\"'Accept all suggested' | translate\"\n [tooltip]=\"'Accept all suggested' | translate\"\n placement=\"right\"\n (click)=\"acceptAllSuggested()\"\n >\n <i [c8yIcon]=\"'check'\"></i>\n </button>\n </div>\n <div class=\"col-md-7 p-l-8\">\n <p\n class=\"text-medium\"\n translate\n >\n Selected source\n </p>\n </div>\n </div>\n </div>\n </div>\n </c8y-li>\n\n <c8y-li\n class=\"c8y-list__item--overflow-visible\"\n *ngFor=\"let widgetAsset of updatedWidgetAssets; let i = index\"\n >\n <c8y-li-icon\n class=\"p-r-0 p-t-16\"\n icon=\"th-large\"\n ></c8y-li-icon>\n <div class=\"d-flex-md\">\n <div class=\"col-md-2\">\n <!-- title -->\n <p\n class=\"text-truncate text-medium boxed-label p-l-0\"\n title=\"{{ widgetAsset.title }}\"\n >\n {{ widgetAsset.title }}\n </p>\n </div>\n <div class=\"col-md-10\">\n <div class=\"row\">\n <div class=\"col-md-4 p-l-md-0 p-r-0\">\n <!-- origin device -->\n <p class=\"visible-sm visible-xs text-label-small\">\n {{ 'Original source' | translate }}\n </p>\n <div class=\"d-flex gap-4 boxed-label\">\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.deviceRef.value }\n \"\n ></ng-container>\n <span class=\"text-truncate\">\n <span\n class=\"text-truncate\"\n title=\"{{ widgetAsset.deviceRef.value.name }}\"\n >\n {{ widgetAsset.deviceRef.value.name }}\n </span>\n <small class=\"text-muted\">\n {{ assetIdInfo | translate: { assetId: widgetAsset.deviceRef.value.id } }}\n </small>\n </span>\n </div>\n </div>\n <div\n class=\"col-md-1 col-xs-1 p-l-xs-0 p-l-sm-0 p-r-sm-0 p-r-xs-0 text-center p-t-16\"\n >\n <i\n class=\"icon-20\"\n [c8yIcon]=\"widgetAsset.selectedAsset !== undefined ? 'ok' : 'inactive-state'\"\n [title]=\"\n widgetAsset.selectedAsset !== undefined\n ? ('Asset selected' | translate)\n : ('Asset not selected' | translate)\n \"\n [ngClass]=\"{\n 'text-success': widgetAsset.selectedAsset !== undefined,\n 'text-muted': !widgetAsset.selectedAsset === undefined\n }\"\n ></i>\n </div>\n <div class=\"col-md-4 col-xs-11 p-l-md-0\">\n <!-- Suggested/Selected source -->\n <p class=\"visible-sm visible-xs text-label-small m-t-16 m-b-8\">\n {{ 'Selected source' | translate }}\n </p>\n <div\n class=\"d-flex gap-4 text-muted boxed-label\"\n *ngIf=\"\n widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n \"\n [attr.data-label]=\"'Suggested' | translate\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.deviceRef.value.suggestedDevice }\n \"\n ></ng-container>\n <span class=\"text-truncate\">\n <span\n class=\"text-truncate\"\n title=\"{{ widgetAsset.deviceRef.value.suggestedDevice?.name }}\"\n >\n {{ widgetAsset.deviceRef.value.suggestedDevice?.name }}\n </span>\n <small class=\"text-muted\">\n {{\n assetIdInfo\n | translate\n : { assetId: widgetAsset.deviceRef.value.suggestedDevice?.id }\n }}\n </small>\n </span>\n </div>\n <div\n class=\"d-flex gap-4 boxed-label\"\n *ngIf=\"widgetAsset.selectedAsset\"\n >\n <!-- selectedAsset icon -->\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.selectedAsset }\n \"\n ></ng-container>\n\n <!-- selectedAsset Name and ID -->\n <span class=\"text-truncate\">\n <span\n class=\"text-truncate\"\n title=\"{{ widgetAsset.selectedAsset?.name }}\"\n >\n {{ widgetAsset.selectedAsset?.name }}\n </span>\n <small class=\"text-muted\">\n {{ assetIdInfo | translate: { assetId: widgetAsset.selectedAsset.id } }}\n </small>\n </span>\n </div>\n\n <div\n class=\"text-muted boxed-label\"\n *ngIf=\"\n !widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n \"\n >\n <em>{{ 'No suggestion available' | translate }}</em>\n </div>\n </div>\n <div class=\"col-md-3 p-0 flex-grow\">\n <div class=\"d-flex a-i-center j-c-end-md gap-8 boxed-label p-l-md-0 p-r-0\">\n <div class=\"input-group max-width-fit\">\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"\n (widgetAsset.deviceRef.value.suggestedDevice\n ? acceptSuggestedLabel\n : noSuggestedAssetLabel\n ) | translate\n \"\n [tooltip]=\"\n (widgetAsset.deviceRef.value.suggestedDevice\n ? acceptSuggestedLabel\n : noSuggestedAssetLabel\n ) | translate\n \"\n placement=\"top\"\n container=\"body\"\n [ngClass]=\"{\n active:\n widgetAsset.selectedAsset &&\n widgetAsset.selectedAsset ===\n widgetAsset.deviceRef.value.suggestedDevice\n }\"\n [disabled]=\"!widgetAsset.deviceRef.value.suggestedDevice\"\n (click)=\"\n widgetAsset.selectedAsset = widgetAsset.deviceRef.value.suggestedDevice\n \"\n >\n <i [c8yIcon]=\"'check'\"></i>\n </button>\n </div>\n <div\n class=\"dropdown input-group-btn\"\n container=\"body\"\n dropdown\n [insideClick]=\"true\"\n #dropdown=\"bs-dropdown\"\n >\n <button\n class=\"btn btn-default dropdown-toggle c8y-dropdown\"\n [attr.aria-label]=\"'Select asset' | translate\"\n tooltip=\"{{ 'Select asset' | translate }}\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n dropdownToggle\n [ngClass]=\"{\n active:\n widgetAsset.selectedAsset &&\n widgetAsset.selectedAsset !==\n widgetAsset.deviceRef.value.suggestedDevice\n }\"\n >\n <i [c8yIcon]=\"'card-exchange'\"></i>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-right--md\"\n style=\"width: 300px\"\n *dropdownMenu\n >\n <c8y-asset-selector-miller\n *ngIf=\"dropdown.isOpen\"\n (onSelected)=\"selectionChange($event, widgetAsset); dropdown.hide()\"\n [asset]=\"contextAsset\"\n [config]=\"{\n groupsSelectable: true,\n showChildDevices: true,\n showFilter: true,\n showSelected: false,\n columnHeaders: true,\n singleColumn: true,\n preventInitialSelect: true\n }\"\n ></c8y-asset-selector-miller>\n </div>\n </div>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Configure later' | translate\"\n tooltip=\"{{ 'Configure later' | translate }}\"\n placement=\"top\"\n container=\"body\"\n [ngClass]=\"{ active: widgetAsset.selectedAsset === null }\"\n (click)=\"widgetAsset.selectedAsset = null\"\n >\n <i [c8yIcon]=\"'link-off'\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </c8y-li>\n </c8y-list-group>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n class=\"btn btn-default\"\n [title]=\"'Cancel' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-default\"\n [title]=\"'Accept all suggested' | translate\"\n type=\"button\"\n (click)=\"acceptAllSuggestedAndApply()\"\n [disabled]=\"!hasSuggestedAssets\"\n >\n {{ 'Accept all suggested' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n [title]=\"'Apply' | translate\"\n type=\"button\"\n (click)=\"apply()\"\n >\n {{ 'Apply' | translate }}\n </button>\n </div>\n</div>\n\n<ng-template\n #assetIcon\n let-device\n>\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'data-transfer'\"\n *ngIf=\"device.c8y_IsDevice\"\n ></i>\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'c8y-group'\"\n *ngIf=\"device.c8y_IsDeviceGroup && !device.c8y_IsAsset\"\n ></i>\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"device.icon?.name || 'c8y-group'\"\n *ngIf=\"device.c8y_IsAsset\"\n ></i>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "component", type: i1.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i1.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i1.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i1.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "ngmodule", type: AssetSelectorModule }, { kind: "component", type: i3.MillerViewComponent, selector: "c8y-asset-selector-miller", inputs: ["config", "asset", "selectedDevice", "rootNode", "container"], outputs: ["onSelected", "onClearSelected"] }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i4.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i4.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i4.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssignWidgetAssetModalComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-assign-widget-asset-modal', standalone: true, imports: [CoreModule, AssetSelectorModule, BsDropdownModule, TooltipModule], template: "<div class=\"viewport-modal has-asset-selector\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'th-large'\"></i>\n <div\n class=\"modal-title\"\n id=\"modal-title\"\n translate\n >\n Select and confirm widget assets mapping\n </div>\n </div>\n <div\n class=\"inner-scroll\"\n id=\"modal-body\"\n >\n <div class=\"p-16\">\n <p class=\"text-center text-balance\">\n {{ modalTitle | translate }}\n </p>\n </div>\n <div\n class=\"d-flex fit-w j-c-center p-16 p-t-0\"\n *ngIf=\"notSupportedWidgets.length\"\n >\n <div class=\"alert alert-warning\">\n {{\n 'Widgets not supported for asset mapping: {{ notSupportedWidgets\n\n\n\n }}' | translate: { notSupportedWidgets: notSupportedWidgets.join(', ') } }}\n </div>\n </div>\n <div\n class=\"p-16 d-flex j-c-center\"\n *ngIf=\"!assetsToAlign.length\"\n >\n <c8y-ui-empty-state\n class=\"\"\n [icon]=\"'empty-box'\"\n [title]=\"'No assets to assign' | translate\"\n ></c8y-ui-empty-state>\n </div>\n <div *ngIf=\"assetsToAlign.length\">\n <c8y-list-group class=\"separator-top no-border-last\">\n <c8y-li class=\"sticky-top bg-level-1 hidden-sm hidden-xs\">\n <c8y-li-icon></c8y-li-icon>\n <div class=\"d-flex\">\n <div class=\"col-md-2\">\n <!-- title -->\n <p\n class=\"text-truncate text-medium\"\n translate\n >\n Widget\n </p>\n </div>\n <div class=\"col-md-10\">\n <div class=\"row\">\n <div class=\"col-md-4 p-r-0\">\n <p\n class=\"text-medium\"\n translate\n >\n Original source\n </p>\n </div>\n <div class=\"col-md-1\">\n <button\n class=\"btn btn-default btn-sm\"\n [attr.aria-label]=\"'Accept all suggested' | translate\"\n [tooltip]=\"'Accept all suggested' | translate\"\n placement=\"right\"\n (click)=\"acceptAllSuggested()\"\n >\n <i [c8yIcon]=\"'check'\"></i>\n </button>\n </div>\n <div class=\"col-md-7 p-l-8\">\n <p\n class=\"text-medium\"\n translate\n >\n Selected source\n </p>\n </div>\n </div>\n </div>\n </div>\n </c8y-li>\n\n <c8y-li\n class=\"c8y-list__item--overflow-visible\"\n *ngFor=\"let widgetAsset of updatedWidgetAssets; let i = index\"\n >\n <c8y-li-icon\n class=\"p-r-0 p-t-16\"\n icon=\"th-large\"\n ></c8y-li-icon>\n <div class=\"d-flex-md\">\n <div class=\"col-md-2\">\n <!-- title -->\n <p\n class=\"text-truncate text-medium boxed-label p-l-0\"\n title=\"{{ widgetAsset.title }}\"\n >\n {{ widgetAsset.title }}\n </p>\n </div>\n <div class=\"col-md-10\">\n <div class=\"row\">\n <div class=\"col-md-4 p-l-md-0 p-r-0\">\n <!-- origin device -->\n <p class=\"visible-sm visible-xs text-label-small\">\n {{ 'Original source' | translate }}\n </p>\n <div class=\"d-flex gap-4 boxed-label\">\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.deviceRef.value }\n \"\n ></ng-container>\n <span class=\"text-truncate\">\n <span\n class=\"text-truncate\"\n title=\"{{ widgetAsset.deviceRef.value.name }}\"\n >\n {{ widgetAsset.deviceRef.value.name }}\n </span>\n <small class=\"text-muted\">\n {{ assetIdInfo | translate: { assetId: widgetAsset.deviceRef.value.id } }}\n </small>\n </span>\n </div>\n </div>\n <div\n class=\"col-md-1 col-xs-1 p-l-xs-0 p-l-sm-0 p-r-sm-0 p-r-xs-0 text-center p-t-16\"\n >\n <i\n class=\"icon-20\"\n [c8yIcon]=\"widgetAsset.selectedAsset !== undefined ? 'ok' : 'inactive-state'\"\n [title]=\"\n widgetAsset.selectedAsset !== undefined\n ? ('Asset selected' | translate)\n : ('Asset not selected' | translate)\n \"\n [ngClass]=\"{\n 'text-success': widgetAsset.selectedAsset !== undefined,\n 'text-muted': !widgetAsset.selectedAsset === undefined\n }\"\n ></i>\n </div>\n <div class=\"col-md-4 col-xs-11 p-l-md-0\">\n <!-- Suggested/Selected source -->\n <p class=\"visible-sm visible-xs text-label-small m-t-16 m-b-8\">\n {{ 'Selected source' | translate }}\n </p>\n <div\n class=\"d-flex gap-4 text-muted boxed-label\"\n *ngIf=\"\n widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n \"\n [attr.data-label]=\"'Suggested' | translate\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.deviceRef.value.suggestedDevice }\n \"\n ></ng-container>\n <span class=\"text-truncate\">\n <span\n class=\"text-truncate\"\n title=\"{{ widgetAsset.deviceRef.value.suggestedDevice?.name }}\"\n >\n {{ widgetAsset.deviceRef.value.suggestedDevice?.name }}\n </span>\n <small class=\"text-muted\">\n {{\n assetIdInfo\n | translate\n : { assetId: widgetAsset.deviceRef.value.suggestedDevice?.id }\n }}\n </small>\n </span>\n </div>\n <div\n class=\"d-flex gap-4 boxed-label\"\n *ngIf=\"widgetAsset.selectedAsset\"\n >\n <!-- selectedAsset icon -->\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.selectedAsset }\n \"\n ></ng-container>\n\n <!-- selectedAsset Name and ID -->\n <span class=\"text-truncate\">\n <span\n class=\"text-truncate\"\n title=\"{{ widgetAsset.selectedAsset?.name }}\"\n >\n {{ widgetAsset.selectedAsset?.name }}\n </span>\n <small class=\"text-muted\">\n {{ assetIdInfo | translate: { assetId: widgetAsset.selectedAsset.id } }}\n </small>\n </span>\n </div>\n\n <div\n class=\"text-muted boxed-label\"\n *ngIf=\"\n !widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n \"\n >\n <em>{{ 'No suggestion available' | translate }}</em>\n </div>\n </div>\n <div class=\"col-md-3 p-0 flex-grow\">\n <div class=\"d-flex a-i-center j-c-end-md gap-8 boxed-label p-l-md-0 p-r-0\">\n <div class=\"input-group max-width-fit\">\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"\n (widgetAsset.deviceRef.value.suggestedDevice\n ? acceptSuggestedLabel\n : noSuggestedAssetLabel\n ) | translate\n \"\n [tooltip]=\"\n (widgetAsset.deviceRef.value.suggestedDevice\n ? acceptSuggestedLabel\n : noSuggestedAssetLabel\n ) | translate\n \"\n placement=\"top\"\n container=\"body\"\n [ngClass]=\"{\n active:\n widgetAsset.selectedAsset &&\n widgetAsset.selectedAsset ===\n widgetAsset.deviceRef.value.suggestedDevice\n }\"\n [disabled]=\"!widgetAsset.deviceRef.value.suggestedDevice\"\n (click)=\"\n widgetAsset.selectedAsset = widgetAsset.deviceRef.value.suggestedDevice\n \"\n >\n <i [c8yIcon]=\"'check'\"></i>\n </button>\n </div>\n <div\n class=\"dropdown input-group-btn\"\n container=\"body\"\n dropdown\n [insideClick]=\"true\"\n #dropdown=\"bs-dropdown\"\n >\n <button\n class=\"btn btn-default dropdown-toggle c8y-dropdown\"\n [attr.aria-label]=\"'Select asset' | translate\"\n tooltip=\"{{ 'Select asset' | translate }}\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n dropdownToggle\n [ngClass]=\"{\n active:\n widgetAsset.selectedAsset &&\n widgetAsset.selectedAsset !==\n widgetAsset.deviceRef.value.suggestedDevice\n }\"\n >\n <i [c8yIcon]=\"'card-exchange'\"></i>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-right--md\"\n style=\"width: 300px\"\n *dropdownMenu\n >\n <c8y-asset-selector-miller\n *ngIf=\"dropdown.isOpen\"\n (onSelected)=\"selectionChange($event, widgetAsset); dropdown.hide()\"\n [asset]=\"contextAsset\"\n [config]=\"{\n groupsSelectable: true,\n showChildDevices: true,\n showFilter: true,\n showSelected: false,\n columnHeaders: true,\n singleColumn: true,\n preventInitialSelect: true\n }\"\n ></c8y-asset-selector-miller>\n </div>\n </div>\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Configure later' | translate\"\n tooltip=\"{{ 'Configure later' | translate }}\"\n placement=\"top\"\n container=\"body\"\n [ngClass]=\"{ active: widgetAsset.selectedAsset === null }\"\n (click)=\"widgetAsset.selectedAsset = null\"\n >\n <i [c8yIcon]=\"'link-off'\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </c8y-li>\n </c8y-list-group>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n class=\"btn btn-default\"\n [title]=\"'Cancel' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-default\"\n [title]=\"'Accept all suggested' | translate\"\n type=\"button\"\n (click)=\"acceptAllSuggestedAndApply()\"\n [disabled]=\"!hasSuggestedAssets\"\n >\n {{ 'Accept all suggested' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n [title]=\"'Apply' | translate\"\n type=\"button\"\n (click)=\"apply()\"\n >\n {{ 'Apply' | translate }}\n </button>\n </div>\n</div>\n\n<ng-template\n #assetIcon\n let-device\n>\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'data-transfer'\"\n *ngIf=\"device.c8y_IsDevice\"\n ></i>\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'c8y-group'\"\n *ngIf=\"device.c8y_IsDeviceGroup && !device.c8y_IsAsset\"\n ></i>\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"device.icon?.name || 'c8y-group'\"\n *ngIf=\"device.c8y_IsAsset\"\n ></i>\n</ng-template>\n" }]
}], propDecorators: { assetsToAlign: [{
type: Input
}], contextAsset: [{
type: Input
}], notSupportedWidgets: [{
type: Input
}] } });
class ImportExportWidgetsService {
constructor() {
this.dynamicComponentService = inject(DynamicComponentService);
this.injector = inject(Injector);
this.alertService = inject(AlertService);
this.dashboardDetailService = inject(DashboardDetailService);
this.modal = inject(BsModalService);
}
/**
* Export the dashboard to a JSON file. If the widget has an export method, it will be called to export
* the widget's configuration.
* @param dashboard Context dashboard to be exported
*/
async export(dashboard) {
const copiedDashboard = cloneDeep(dashboard);
for (const [id, child] of Object.entries(copiedDashboard.children || {})) {
try {
const definition = await this.dynamicComponentService.getById(child.componentId);
if (definition && typeof definition.data.export === 'function') {
const widgetData = definition.data;
const pluginInjector = definition.injector || this.injector;
await runInInjectionContext(pluginInjector, async () => {
const exportedConfig = await widgetData.export(child.config, this.dashboardDetailService.details, { pluginInjector, injector: this.injector });
copiedDashboard.children[id].config = exportedConfig;
});
}
}
catch (error) {
this.alertService.addServerFailure(error);
}
}
const dateStr = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `dashboard_${dateStr}.json`;
const blob = new Blob([JSON.stringify(copiedDashboard)], { type: 'application/json' });
saveAs(blob, filename);
this.alertService.success(gettext('Dashboard exported.'));
}
/**
* Import the dashboard from a JSON file. If the widget has an import method, it will be called to import
* widget's configuration.
* @param file JSON file with context dashboard configuration
*/
async import(file) {
if (!file || file.length === 0) {
return;
}
const dashboard = await file[0].readAsJson();
const notSupportedWidgets = [];
for (const [id, child] of Object.entries(dashboard.children)) {
try {
const definition = await this.dynamicComponentService.getById(child.componentId);
if (definition && typeof definition.data.import === 'function') {
const widgetData = definition.data;
const pluginInjector = definition.injector || this.injector;
await runInInjectionContext(pluginInjector, async () => {
const importedConfig = await widgetData.import(child.config, this.dashboardDetailService.details, { pluginInjector, injector: this.injector });
dashboard.children[id].config = importedConfig;
});
}
else {
notSupportedWidgets.push(child.title);
}
}
catch (error) {
console.error(`Error importing widget with id ${id}:`, error);
}
}
if (Object.entries(dashboard.children).length) {
dashboard.children = await this.assignWidgetAssets(dashboard.children, notSupportedWidgets);
if (!dashboard.children) {
return;
}
}
return JSON.stringify(dashboard, null, 2);
}
async assignWidgetAssets(widgets, notSupportedWidgets) {
const assetsToAlign = this.findWidgetsWithSuggestedDevice(widgets);
const contextAsset = this.dashboardDetailService.details.context;
try {
const modalRef = this.modal.show(AssignWidgetAssetModalComponent, {
class: 'modal-lg',
ariaDescribedby: 'modal-body',
ariaLabelledBy: 'modal-title',
ignoreBackdropClick: true,
keyboard: false,
initialState: { assetsToAlign, contextAsset, notSupportedWidgets }
}).content;
const result = await modalRef.result;
return this.updateWidgets(widgets, result);
}
catch (_) {
// cancel clicked
return null;
}
}
findWidgetsWithSuggestedDevice(widgets) {
const results = [];
const searchObject = (obj, path = []) => {
if (!obj || typeof obj !== 'object')
return;
for (const [key, value] of Object.entries(obj)) {
const currentPath = [...path, key];
if (key === 'suggestedDevice') {
// Find the nearest device/__target reference
let deviceRef = null;
let devicePath = [];
// Search for __target or device in parent objects
let currentObj = obj;
const currentSearchPath = [...path];
while (currentObj && !deviceRef) {