@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
591 lines • 71.5 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&type=dashboarding!@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.25", ngImport: i0, type: JsonValidationService, deps: [{ token: i1.DynamicComponentService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: JsonValidationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: JsonValidationService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.DynamicComponentService }] });
class AssignWidgetAssetModalComponent {
constructor() {
this.notSupportedWidgets = [];
this.modalTitle = gettext(`
For each asset in the widgets, we suggest a source based on the original configuration.
You can accept it, choose another asset, or configure it later. If you don’t choose any option,
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.userClickedAcceptAllSuggested = false;
this.assetSelectedLabel = gettext('Asset selected');
this.assetNotSelectedLabel = gettext('Asset not selected');
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.userClickedAcceptAllSuggested = true;
this.updatedWidgetAssets.forEach(asset => {
if (asset.deviceRef.value.suggestedDevice) {
asset.selectedAsset = asset.deviceRef.value.suggestedDevice;
}
});
}
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.25", ngImport: i0, type: AssignWidgetAssetModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", 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 @if (notSupportedWidgets.length) {\n <div class=\"d-flex fit-w j-c-center p-16 p-t-0\">\n <div class=\"alert alert-warning\">\n {{ 'Widgets not supported for asset mapping: {{ notSupportedWidgets }}' | translate: {\n notSupportedWidgets: notSupportedWidgets.join(', ') } }}\n </div>\n </div>\n }\n @if (!assetsToAlign.length) {\n <div class=\"p-16 d-flex j-c-center\">\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 }\n @if (assetsToAlign.length) {\n <div>\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 text-center\">\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 [disabled]=\"!hasSuggestedAssets\"\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 @for (widgetAsset of updatedWidgetAssets; track widgetAsset; let i = $index) {\n <c8y-li class=\"c8y-list__item--overflow-visible\">\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 {{\n assetIdInfo | translate: { assetId: widgetAsset.deviceRef.value.id }\n }}\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]=\"\n widgetAsset.selectedAsset !== undefined ? 'ok' : 'inactive-state'\n \"\n [title]=\"\n widgetAsset.selectedAsset !== undefined\n ? (assetSelectedLabel | translate)\n : (assetNotSelectedLabel | translate)\n \"\n [ngClass]=\"{\n 'text-success': widgetAsset.selectedAsset !== undefined,\n 'text-muted': widgetAsset.selectedAsset === undefined\n }\"\n ></i>\n @if (\n widgetAsset.selectedAsset === undefined && userClickedAcceptAllSuggested\n ) {\n <span\n class=\"c8y-icon-badge\"\n [tooltip]=\"\n 'No suggestion available - widget will not be imported' | translate\n \"\n placement=\"right\"\n >\n <i\n class=\"status major stroked-icon\"\n c8yIcon=\"warning\"\n ></i>\n </span>\n }\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 @if (\n widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n ) {\n <div\n class=\"d-flex gap-4 text-muted boxed-label\"\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 }\n @if (widgetAsset.selectedAsset) {\n <div class=\"d-flex gap-4 boxed-label\">\n <!-- selectedAsset icon -->\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.selectedAsset }\n \"\n ></ng-container>\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 {{\n assetIdInfo | translate: { assetId: widgetAsset.selectedAsset.id }\n }}\n </small>\n </span>\n </div>\n }\n @if (\n !widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n ) {\n <div class=\"text-muted boxed-label\">\n <em>{{ 'No suggestion available' | translate }}</em>\n </div>\n }\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 =\n 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 @if (dropdown.isOpen) {\n <c8y-asset-selector-miller\n (onSelected)=\"\n selectionChange($event, widgetAsset); dropdown.hide()\n \"\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 }\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 }\n </c8y-list-group>\n </div>\n }\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-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 @if (device.c8y_IsDevice) {\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'data-transfer'\"\n ></i>\n }\n @if (device.c8y_IsDeviceGroup && !device.c8y_IsAsset) {\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'c8y-group'\"\n ></i>\n }\n @if (device.c8y_IsAsset) {\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"device.icon?.name || 'c8y-group'\"\n ></i>\n }\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.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.25", 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 @if (notSupportedWidgets.length) {\n <div class=\"d-flex fit-w j-c-center p-16 p-t-0\">\n <div class=\"alert alert-warning\">\n {{ 'Widgets not supported for asset mapping: {{ notSupportedWidgets }}' | translate: {\n notSupportedWidgets: notSupportedWidgets.join(', ') } }}\n </div>\n </div>\n }\n @if (!assetsToAlign.length) {\n <div class=\"p-16 d-flex j-c-center\">\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 }\n @if (assetsToAlign.length) {\n <div>\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 text-center\">\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 [disabled]=\"!hasSuggestedAssets\"\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 @for (widgetAsset of updatedWidgetAssets; track widgetAsset; let i = $index) {\n <c8y-li class=\"c8y-list__item--overflow-visible\">\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 {{\n assetIdInfo | translate: { assetId: widgetAsset.deviceRef.value.id }\n }}\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]=\"\n widgetAsset.selectedAsset !== undefined ? 'ok' : 'inactive-state'\n \"\n [title]=\"\n widgetAsset.selectedAsset !== undefined\n ? (assetSelectedLabel | translate)\n : (assetNotSelectedLabel | translate)\n \"\n [ngClass]=\"{\n 'text-success': widgetAsset.selectedAsset !== undefined,\n 'text-muted': widgetAsset.selectedAsset === undefined\n }\"\n ></i>\n @if (\n widgetAsset.selectedAsset === undefined && userClickedAcceptAllSuggested\n ) {\n <span\n class=\"c8y-icon-badge\"\n [tooltip]=\"\n 'No suggestion available - widget will not be imported' | translate\n \"\n placement=\"right\"\n >\n <i\n class=\"status major stroked-icon\"\n c8yIcon=\"warning\"\n ></i>\n </span>\n }\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 @if (\n widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n ) {\n <div\n class=\"d-flex gap-4 text-muted boxed-label\"\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 }\n @if (widgetAsset.selectedAsset) {\n <div class=\"d-flex gap-4 boxed-label\">\n <!-- selectedAsset icon -->\n <ng-container\n *ngTemplateOutlet=\"\n assetIcon;\n context: { $implicit: widgetAsset.selectedAsset }\n \"\n ></ng-container>\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 {{\n assetIdInfo | translate: { assetId: widgetAsset.selectedAsset.id }\n }}\n </small>\n </span>\n </div>\n }\n @if (\n !widgetAsset.deviceRef.value.suggestedDevice && !widgetAsset.selectedAsset\n ) {\n <div class=\"text-muted boxed-label\">\n <em>{{ 'No suggestion available' | translate }}</em>\n </div>\n }\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 =\n 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 @if (dropdown.isOpen) {\n <c8y-asset-selector-miller\n (onSelected)=\"\n selectionChange($event, widgetAsset); dropdown.hide()\n \"\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 }\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 }\n </c8y-list-group>\n </div>\n }\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-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 @if (device.c8y_IsDevice) {\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'data-transfer'\"\n ></i>\n }\n @if (device.c8y_IsDeviceGroup && !device.c8y_IsAsset) {\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"'c8y-group'\"\n ></i>\n }\n @if (device.c8y_IsAsset) {\n <i\n class=\"l-h-inherit\"\n [c8yIcon]=\"device.icon?.name || 'c8y-group'\"\n ></i>\n }\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) {
if (currentObj.__target) {
deviceRef = currentObj.__target;
devicePath = [...currentSearchPath, '__target'];
break;
}
if (currentObj.device) {
deviceRef = currentObj.device;
devicePath = [...currentSearchPath, 'device'];
break;
}
// Move up one level
currentSearchPath.pop();
currentObj = this.getObjectByPath(widgets, currentSearchPath);
}
if (deviceRef) {
const widgetId = path[0]; // First element in path should be widget ID
results.push({
widgetId,
title: widgets[widgetId].title,
deviceRef: { path: devicePath, value: deviceRef }
});
}
}
else if (typeof value === 'object') {
searchObject(value, currentPath);
}
}
};
searchObject(widgets);
return results;
}
getObjectByPath(obj, path) {
return path.reduce((curr, key) => curr && curr[key], obj);
}
updateWidgets(originalWidgets, processedReferences) {
const result = JSON.parse(JSON.stringify(originalWidgets));
// remove widgets that were no aligned
processedReferences.forEach(ref => {
if (ref.deviceRef.value === undefined) {
delete result[ref.widgetId];
return;
}
// apply selected assets to widgets
const obj = this.getObjectByPath(result, ref.deviceRef.path.slice(0, -1));
const lastKey = ref.deviceRef.path[ref.deviceRef.path.length - 1];
if (obj && lastKey) {
obj[lastKey] = ref.deviceRef.value;
delete obj[lastKey]?.suggestedDevice;
}
});
return result;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: ImportExportWidgetsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: ImportExportWidgetsService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: ImportExportWidgetsService, decorators: [{
type: Injectable
}] });
class DashboardJsonEditorComponent {
constructor() {
this.infoText = gettext("Import / Export allows copying dashboard configurations between assets using JSON files. You can export a dashboard's setup and import it to another asset.\n\nNote: Basic knowledge of dashboard JSON structure is required.");
this.valueString = '';
this.errors = signal([], ...(ngDevMode ? [{ debugName: "errors" }] : []));
this.isExporting = false;
this.isImporting = false;
this.editorOptions = {
hover: {
above: false
}
};
this.dashboardDetailService = inject(DashboardDetailService);
this.jsonValidationService = inject(JsonValidationService);
this.importExportWidgetsService = inject(ImportExportWidgetsService);
this.valueChange = new Subject();
this.destroy$ = new Subject();
this.currentSchemas = null;
}
ngOnInit() {
this.dashboard = this.dashboardDetailService.details.dashboard;
this.editorTheme = this.getEditorTheme();
this.valueString = JSON.stringify(this.dashboard, null, 2);
this.valueChange.pipe(debounceTime(500), takeUntil(this.destroy$)).subscribe(() => {
this.applyChanges();
this.validate();
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
dashboardJSONChange(value) {
this.valueString = value;
this.valueChange.next();
}
async assignSchema() {
this.currentSchemas = await this.jsonValidationService.getJsonSchemas(this.dashboard);
this.editorComponent.monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: this.currentSchemas,
enableSchemaRequest: false,
allowComments: false
});
this.validate();
}
async validate() {
this.editorDirty = this.valueString !== JSON.stringify(this.dashboard, null, 2);
try {
const validationResult = await this.jsonValidationService.validateDashboard(this.valueString, this.currentSchemas);
this.errors.set(validationResult);
}
catch (error) {
console.error('Validation error:', error);
this.errors.set([
{
message: 'An error occurred during validation',
path: ''
}
]);
}
}
applyChanges() {
try {
const newDashboard = JSON.parse(this.valueString);
this.dashboardDetailService.details.dashboard = newDashboard;
this.dashboardDetailService.generalSettingsForm.patchValue(newDashboard);
this.dashboardDetailService.appearanceSettingsForm.patchValue(newDashboard);
this.dashboardDetailService.widgetsForm.patchValue(newDashboard);
this.dashboardDetailService.previewChangedFn({
classes: newDashboard.classes,
widgetClasses: newDashboard.widgetClasses
});
this.dashboardDetailService.dashboardDetailsForm.markAsDirty();
this.dashboardDetailService.saveButtonDisabled = false;
}
catch (error) {
// disable save button if JSON is invalid and don't apply value to form
this.dashboardDetailService.saveButtonDisabled = true;
}
}
async export() {
this.isExporting = true;
try {
await this.importExportWidgetsService.export(this.dashboard);
}
finally {
this.isExporting = false;
}
}
async import(file) {
this.isImporting = true;
try {
if (file && file.length !== 0) {
const importedValue = await this.importExportWidgetsService.import(file);
if (importedValue) {
this.valueString = importedValue;
this.valueChange.next();
}
}
}
finally {
this.isImporting = false;
}
}
getEditorTheme() {
const dashboardClasses = this.dashboardDetailService.appearanceSettingsForm.get('classes').value;
if (dashboardClasses['dashboard-theme-dark']) {
return 'dark';
}
else if (dashboardClasses['dashboard-theme-white']) {
return 'light';
}
return null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: DashboardJsonEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.25", type: DashboardJsonEditorComponent, isStandalone: true, selector: "c8y-dashboard-json-editor", providers: [JsonValidationService, ImportExportWidgetsService], viewQueries: [{ propertyName: "editorComponent", first: true, predicate: EditorComponent, descendants: true }], ngImport: i0, template: "<div class=\"card-block\">\n<div\n class=\"d-flex d-col\"\n [ngStyle]=\"{ height: '410px' }\"\n>\n <ng-container *ngIf=\"!isExporting && !isImporting; else loading\">\n <div class=\"d-flex gap-16 flex-grow min-height-0\">\n <div class=\"d-col col-md-8 col-xs-6\">\n <label class=\"d-flex a-i-center\">{{ 'Dashboard configuration' | translate }} \n <button \n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n class=\"btn-help \" \n placement=\"right\"\n triggers=\"focus\"\n containerClass=\"text-pre-wrap\"\n [popover]=\"infoText | translate\"\n ></button>\n </label>\n <c8y-editor\n class=\"flex-grow min-height-0\"\n [ngModel]=\"valueString\"\n (ngModelChange)=\"dashboardJSONChange($event)\"\n (editorInit)=\"assignSchema()\"\n [theme]=\"editorTheme\"\n monacoEditorMarkerValidator\n [editorOptions]=\"editorOptions\"\n ></c8y-editor>\n </div>\n <div class=\"col-md-4 d-col col-xs-6\">\n <label translate>Validation</label>\n <div\n class=\"c8y-messages has-error m-b-24 inner-scroll flex-grow\"\n *ngIf=\"errors().length > 0\"\n >\n <div\n class=\"form-control-feedback-message\"\n *ngFor=\"let error of errors()\"\n >\n <span\n class=\"error-path\"\n *ngIf=\"error.path\"\n >\n {{ error.path }}:\n </span>\n <span class=\"error-message\">{{ error.message }}</span>\n </div>\n </div>\n <div\n class=\"c8y-messages has-success m-b-24\"\n *ngIf=\"errors().length === 0\"\n >\n <div\n class=\"form-control-feedback-message\"\n >\n <span class=\"error-message\">{{ 'No errors found' | translate }}</span>\n </div>\n </div>\n <div class=\"d-flex gap-16 a-i-center flex-no-shrink min-height-0 m-t-auto\">\n <c8y-drop-area\n class=\"drop-area-sm min-width-0\"\n [icon]=\"'upload'\"\n *ngIf=\"!isExporting\"\n [accept]=\"'.json'\"\n [maxAllowedFiles]=\"1\"\n [message]=\"'Import JSON' | translate\"\n (dropped)=\"import($event)\"\n [loading]=\"isImporting\"\n ></c8y-drop-area>\n <button\n class=\"btn btn-primary\"\n (click)=\"export()\"\n [disabled]=\"isExporting || isImporting\"\n [ngClass]=\"{\n 'btn-pending': isExporting\n }\"\n >\n {{'Export JSON' | translate}}\n </button>\n </div>\n </div>\n </div>\n </ng-container>\n\n</div>\n\n<ng-template #loading>\n <c8y-loading></c8y-loading>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i1.LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: CoreModule }, { kind: "component", type: i1.DropAreaComponent, selector: "c8y-drop-area", inputs: ["formControl", "title", "message", "icon", "loadingMessage", "forceHideList", "alwaysShow", "clickToOpen", "loading", "progress", "maxAllowedFiles", "files", "maxFileSizeInMegaBytes", "accept"], outputs: ["dropped"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i4$1.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "component", type: EditorComponent, selector: "c8y-editor", inputs: ["editorOptions", "theme"], outputs: ["editorInit"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: DashboardJsonEditorComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-dashboard-json-editor', standalone: true, imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
CoreModule,
PopoverModule,
EditorComponent
], providers: [JsonValidationService, ImportExportWidgetsService], template: "<div class=\"card-block\">\n<div\n class=\"d-flex d-col\"\n [ngStyle]=\"{ height: '410px' }\"\n>\n <ng-container *ngIf=\"!isExporting && !isImporting; else loading\">\n <div class=\"d-flex gap-16 flex-grow min-height-0\">\n <div class=\"d-col col-md-8 col-xs-6\">\n <label class=\"d-flex a-i-center\">{{ 'Dashboard configuration' | translate }} \n <button \n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n class=\"btn-help \" \n placement=\"right\"\n triggers=\"focus\"\n containerClass=\"text-pre-wrap\"\n [popover]=\"infoText | translate\"\n ></button>\n </label>\n <c8y-editor\n class=\"flex-grow min-height-0\"\n [ngModel]=\"valueString\"\n (ngModelChange)=\"dashboardJSONChange($event)\"\n (editorInit)=\"assignSchema()\"\n [theme]=\"editorTheme\"\n monacoEditorMarkerValidator\n [editorOptions]=\"editorOptions\"\n ></c8y-editor>\n </div>\n <div class=\"col-md-4 d-col col-xs-6\">\n <label translate>Validation</label>\n <div\n class=\"c8y-messages has-error m-b-24 inner-scroll flex-grow\"\n *ngIf=\"errors().length > 0\"\n >\n <div\n class=\"form-control-feedback-message\"\n *ngFor=\"let error of errors()\"\n >\n <span\n class=\"error-path\"\n *ngIf=\"error.path\"\n >\n {{ error.path }}:\n </span>\n <span class=\"error-message\">{{ error.message }}</span>\n </div>\n </div>\n <div\n class=\"c8y-messages has-success m-b-24\"\n *ngIf=\"errors().length === 0\"\n >\n <div\n class=\"form-control-feedback-message\"\n >\n <span class=\"error-message\">{{ 'No errors found' | translate }}</span>\n </div>\n </div>\n <div class=\"d-flex gap-16 a-i-center flex-no-shrink min-height-0 m-t-auto\">\n <c8y-drop-area\n class=\"drop-area-sm min-width-0\"\n [icon]=\"'upload'\"\n *ngIf=\"!isExporting\"\n [accept]=\"'.json'\"\n [maxAllowedFiles]=\"1\"\n [message]=\"'Import JSON' | translate\"\n (dropped)=\"import($event)\"\n [loading]=\"isImporting\"\n ></c8y-drop-area>\n <button\n class=\"btn btn-primary\"\n (click)=\"export()\"\n [disabled]=\"isExporting || isImporting\"\n [ngClass]=\"{\n 'btn-pending': isExporting\n }\"\n >\n {{'Export JSON' | translate}}\n </button>\n </div>\n </div>\n </div>\n </ng-container>\n\n</div>\n\n<ng-template #loading>\n <c8y-loading></c8y-loading>\n</ng-template>\n" }]
}], propDecorators: { editorComponent: [{
type: ViewChild,
args: [EditorComponent]
}] } });
class DashboardDetailsAdvancedTabComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: DashboardDetailsAdvancedTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.25", type: DashboardDetailsAdvancedTabComponent, isStandalone: true, selector: "app-dashboard-details-advanced-tab", ngImport: i0, template: `<c8y-dashboard-json-editor></c8y-dashboard-json-editor>`, isInline: true, dependencies: [{ kind: "component", type: DashboardJsonEditorComponent, selector: "c8y-dashboard-json-editor" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: DashboardDetailsAdvancedTabComponent, decorators: [{
type: Component,
args: [{
selector: 'app-dashboard-details-advanced-tab',
template: `<c8y-dashboard-json-editor></c8y-dashboard-json-editor>`,
standalone: true,
imports: [DashboardJsonEditorComponent]
}]
}] });
export { DashboardDetailsAdvancedTabComponent };
//# sourceMappingURL=c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-L0Ns8oOr.mjs.map