@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 376 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-context-dashboard.mjs","sources":["../../context-dashboard/new-dashboard.guard.ts","../../context-dashboard/add-dashboard.component.ts","../../context-dashboard/add-dashboard.component.html","../../context-dashboard/add-dashboard.factory.ts","../../context-dashboard/context-dashboard.model.ts","../../context-dashboard/context-dashboard.service.ts","../../context-dashboard/dashboard-detail.service.ts","../../context-dashboard/dashboard-detail.component.ts","../../context-dashboard/dashboard-detail.component.html","../../context-dashboard/memento/dashboard-originator.service.ts","../../context-dashboard/memento/dashboard-caretaker.service.ts","../../context-dashboard/memento/dashboard-edit-mode.service.ts","../../context-dashboard/widget-config-hook/widget-config-hook.model.ts","../../context-dashboard/widget-config-hook/widget-config-hook.service.ts","../../context-dashboard/widget-config/widget-config-section.component.ts","../../context-dashboard/widget-config/widget-config-section.component.html","../../context-dashboard/widget-config/widget-config-feedback.component.ts","../../context-dashboard/widget-config/widget-config-feedback.component.html","../../context-dashboard/widget-config/widget-asset-selector.component.ts","../../context-dashboard/widget-config/widget-asset-selector.component.html","../../context-dashboard/widget-config/widget-config-general.component.ts","../../context-dashboard/widget-config/widget-config-general.component.html","../../context-dashboard/widget-config.service.ts","../../context-dashboard/widget-config/appearance-settings.component.ts","../../context-dashboard/widget-config/appearance-settings.component.html","../../context-dashboard/widget-config/widget-config-root.component.ts","../../context-dashboard/widget-config/widget-config-root.component.html","../../context-dashboard/widget-config/widget-preview.component.ts","../../context-dashboard/widget-config/widget-preview.component.html","../../context-dashboard/widget.service.ts","../../context-dashboard/widget-config.component.ts","../../context-dashboard/widget-config.component.html","../../context-dashboard/context-dashboard.component.ts","../../context-dashboard/context-dashboard.component.html","../../context-dashboard/dashboard-details-tabs.factory.ts","../../context-dashboard/paste-dashboard-action.component.ts","../../context-dashboard/type-dashboard-info/type-dashboard-info.component.ts","../../context-dashboard/type-dashboard-info/type-dashboard-info.component.html","../../context-dashboard/widget-config/widget-preview-wrapper.component.ts","../../context-dashboard/context-dashboard.module.ts","../../context-dashboard/dashboard-action-bar.factory.ts","../../context-dashboard/device-info-dashboard/device-info-dashboard.component.ts","../../context-dashboard/device-info-dashboard/device-info-dashboard.component.html","../../context-dashboard/device-info-dashboard/device-info-dashboard.module.ts","../../context-dashboard/device-management-home-dashboard/device-management-home-dashboard.component.ts","../../context-dashboard/device-management-home-dashboard/device-management-home-dashboard.component.html","../../context-dashboard/device-management-home-dashboard/device-management-home-dashboard.module.ts","../../context-dashboard/widget-config/widget-config-appearance.component.ts","../../context-dashboard/widget-config/widget-config-appearance.component.html","../../context-dashboard/widget-config/global-context-section.component.ts","../../context-dashboard/widget-config/global-context-section.component.html","../../context-dashboard/c8y-ngx-components-context-dashboard.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { Tab } from '@c8y/ngx-components';\nimport { ActivatedRouteSnapshot } from '@angular/router';\nimport { ContextDashboardManagedObject } from './context-dashboard.model';\n\nexport const newDashboardTab: Tab<string> = {\n featureId: 'newDashboard',\n icon: 'th',\n label: gettext('New dashboard'),\n path: 'new-dashboard',\n // place tab as last one and hide it so it won't be opened until user initiates adding new dashboard\n hide: true,\n priority: -Infinity\n};\n\n@Injectable({ providedIn: 'root' })\nexport class NewDashboardGuard {\n tab: Tab;\n\n canActivate(route: ActivatedRouteSnapshot): Observable<boolean | Tab[]> {\n const tabActive = route.routeConfig.path === newDashboardTab.path;\n if (tabActive) {\n const dashboard: Partial<ContextDashboardManagedObject> = {\n c8y_Dashboard: null\n };\n route.data = { dashboard };\n }\n if (!this.tab) {\n this.tab = {\n ...newDashboardTab,\n hide: !tabActive,\n priority: tabActive ? Infinity : -Infinity\n };\n }\n return of([this.tab]);\n }\n}\n","import { Component } from '@angular/core';\nimport { TabsService, IconDirective, C8yTranslatePipe } from '@c8y/ngx-components';\nimport { Router } from '@angular/router';\nimport { newDashboardTab } from './new-dashboard.guard';\n\n@Component({\n selector: '[c8y-add-dashboard]',\n templateUrl: './add-dashboard.component.html',\n host: { class: 'd-flex a-i-stretch sticky-right' },\n imports: [IconDirective, C8yTranslatePipe]\n})\nexport class AddDashboardComponent {\n constructor(\n private tabsService: TabsService,\n private router: Router\n ) {}\n\n addDashboard() {\n const tempNewDashboardTab = [...this.tabsService.state].find(\n t => t.featureId === newDashboardTab.featureId\n );\n // navigate before tab is displayed, because in DashboardDetailComponent tab is hidden on navigation from it.\n this.router.navigate(\n typeof tempNewDashboardTab.path === 'string'\n ? [tempNewDashboardTab.path]\n : tempNewDashboardTab.path,\n { replaceUrl: true }\n );\n // show tab and make it appear as first one\n tempNewDashboardTab.hide = false;\n tempNewDashboardTab.priority = Infinity;\n this.tabsService.refresh();\n }\n}\n","<div class=\"d-flex a-i-stretch m-b-8 m-t-8 p-l-8 hidden-xs\">\n <button\n class=\"btn btn-default btn-sm p-l-8 p-r-8 fit-h p-b-0 p-t-0 d-flex a-i-center\"\n title=\"{{ 'Add dashboard' | translate }}\"\n type=\"button\"\n (click)=\"addDashboard()\"\n >\n <i\n class=\"icon-20 m-r-4\"\n c8yIcon=\"add-circle-outline\"\n ></i>\n <span>{{ 'Add dashboard' | translate }}</span>\n </button>\n <div class=\"p-r-sm-40\"></div>\n</div>\n","import { inject } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { IIdentified } from '@c8y/client';\nimport {\n ContextData,\n ContextRouteService,\n ExtensionFactory,\n Permissions,\n Tab,\n ViewContext\n} from '@c8y/ngx-components';\nimport { AddDashboardComponent } from './add-dashboard.component';\n\nexport abstract class AddDashboardFactory implements ExtensionFactory<Tab> {\n protected abstract targetContext: ViewContext.Device | ViewContext.Group;\n currentContext: ContextData;\n\n private permissions = inject(Permissions);\n private contextRoute = inject(ContextRouteService);\n\n async get(activatedRoute?: ActivatedRoute): Promise<Tab | Tab[]> {\n this.currentContext = this.contextRoute.getContextData(activatedRoute);\n if (\n this.currentContext?.context === this.targetContext &&\n (await this.hasPermission(this.currentContext.contextData))\n ) {\n return [\n {\n component: AddDashboardComponent,\n priority: -Infinity,\n showAlways: true\n }\n ];\n }\n return [];\n }\n\n private async hasPermission(context: IIdentified) {\n if (context?.id) {\n return await this.permissions.canEdit(\n [\n Permissions.ROLE_INVENTORY_ADMIN,\n Permissions.ROLE_INVENTORY_CREATE,\n Permissions.ROLE_MANAGED_OBJECT_ADMIN,\n Permissions.ROLE_MANAGED_OBJECT_CREATE\n ],\n context\n );\n }\n return this.permissions.hasAnyRole([\n Permissions.ROLE_INVENTORY_ADMIN,\n Permissions.ROLE_INVENTORY_CREATE,\n Permissions.ROLE_MANAGED_OBJECT_ADMIN,\n Permissions.ROLE_MANAGED_OBJECT_CREATE\n ]);\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport { IManagedObject } from '@c8y/client';\nimport {\n Widget,\n DynamicComponentDefinition,\n Route,\n ViewContext,\n WidgetDisplaySettings,\n TabWithTemplate,\n WidgetSettings\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { Observable } from 'rxjs/internal/Observable';\nimport { SupportedIconsSuggestions } from '@c8y/ngx-components/icon-selector/icons';\n\nexport const CONTEXT_DASHBOARD_CONFIG = new InjectionToken<any>('ContextDashboardConfig');\nexport const DASHBOARD_SETTINGS_CHANGES = {\n classes: gettext('theme'),\n globalRolesIds: gettext('global roles'),\n widgetClasses: gettext('widget header style'),\n widgetMargin: gettext('widget margin'),\n icon: gettext('icon'),\n name: gettext('name'),\n priority: gettext('priority'),\n c8y_IsNavigatorNode: gettext('navigator item'),\n translateWidgetTitle: gettext('translate widget title'),\n translateDashboardTitle: gettext('translate dashboard title'),\n children: gettext('widgets')\n} as const satisfies Partial<Record<keyof ContextDashboard, string>>;\n\nexport interface ContextDashboardConfig {\n widgetFilter?: (component: DynamicComponentDefinition) => boolean;\n allowFullscreen?: boolean;\n /**\n * @deprecated\n */\n routes?: Route[];\n}\n\nexport interface ContextWidgetConfig {\n /**\n * Settings that define how the default config component is\n * displayed. They are static and will not be saved.\n */\n settings?: WidgetSettings;\n\n /**\n * Settings that are injected in any displaying component.\n */\n displaySettings?: WidgetDisplaySettings;\n\n /**\n * Whatever should be added to the configuration when a widget is created.\n */\n config?: {\n /**\n * Any other information that should be stored here.\n */\n [key: string]: any;\n };\n /**\n * The selected device or group (note: naming is inconsistent as group was added later\n * but must stay for already implemented widgets)\n */\n device?: {\n id?: string | number;\n name?: string;\n [key: string]: any;\n };\n /**\n * Method to export the widget configuration during dashboard export to a json file. It enhances the configuration with\n * additional data that can be used later by the `import` method to restore the widget configuration in a new context.\n * @param config Widget configuration\n * @return Enhanced widget configuration\n */\n export?: (config: any) => any | Promise<any>;\n /**\n * Method to import the widget configuration during dashboard import from a json file. It restores the widget configuration\n * with data exported by the `export` method.\n * @param config Widget configuration enhanced with export method\n * @param dashboardData Dashboard metadata\n * @return Restored widget configuration\n */\n import?: (config: any, dashboardData: DashboardMetadata) => any | Promise<any>;\n\n /**\n * Any other information that should be stored here.\n */\n [key: string]: any;\n}\n\nexport interface ContextDashboardManagedObject extends IManagedObject {\n c8y_Dashboard?: ContextDashboard;\n name?: string;\n c8y_DashboardHistory?: ContextDashboard[];\n}\n\nexport interface ContextDashboard {\n icon?: SupportedIconsSuggestions | null;\n name?: string | null;\n priority?: number | null;\n deviceType?: boolean | null;\n deviceTypeValue?: string | null;\n isFrozen?: boolean | null;\n classes?: { [key: string]: boolean } | null;\n widgetClasses?: { [key: string]: boolean } | null;\n widgetMargin?: number | null;\n translateWidgetTitle?: boolean | null;\n translateDashboardTitle?: boolean | null;\n global?: boolean | null;\n /**\n * The amount of columns on that dashboard.\n * Can be freely chosen, but product uses either 12 or 24.\n */\n columns?: number | null;\n children?: {\n [id: string]: Widget;\n };\n globalRolesIds?: DashboardGlobalRoles | null;\n c8y_IsNavigatorNode?: object | null;\n description?: string | null;\n historyDescription?: DashboardHistoryDescription | null;\n created?: string | null;\n author?: string | null;\n dashboardState?: { [key: string]: any };\n}\n/**\n * Object describing changes applied to dashboard settings and its widgets. Used to display user-friendly change log.\n */\nexport interface DashboardHistoryDescription {\n /**\n * Indicates type of dashboard change (or creation).\n */\n changeType?: 'reset' | 'create' | 'update' | null;\n /**\n * List of dashboard settings that has been changed.\n */\n dashboardSettingChanges?: (typeof DASHBOARD_SETTINGS_CHANGES)[keyof typeof DASHBOARD_SETTINGS_CHANGES][];\n /**\n * True if dashboard is typed dashboard, false if it's not.\n */\n deviceType?: boolean | null;\n /**\n * Object containing lists of widgets (by title) that has been changed, grouped by change type, e.g.:\n * ```ts\n * widgetChanges: {\n * removed: ['Applications'],\n * config?: ['Data points graph', 'Events list'],\n * },\n * ```\n */\n widgetChanges?: {\n removed?: string[];\n added?: string[];\n config?: string[];\n arrangement?: string[];\n } | null;\n /**\n * String used to display the date from which the state was restored.\n */\n restored?: string;\n}\n\nexport const DASHBOARD_CHILDREN_STATE_NAME = {\n initial: gettext('Initial state'),\n config: gettext('Widget configuration changed'),\n removed: gettext('Widget removed'),\n added: gettext('Widget added'),\n arrangement: gettext('Widgets rearranged')\n} as const satisfies Record<keyof DashboardHistoryDescription['widgetChanges'] | 'initial', string>;\n/**\n * Object representing state of dashboard widgets. Its main purpose is to allow to undo and redo changes\n * applied to dashboard children.\n */\nexport type DashboardChildrenState = {\n /**\n * Name of the change applied to dashboard that results in current state, e.g. 'widget removed'\n */\n name: (typeof DASHBOARD_CHILDREN_STATE_NAME)[keyof typeof DASHBOARD_CHILDREN_STATE_NAME];\n /**\n * Dashboard children in particular, immutable state.\n */\n children: ContextDashboard['children'];\n /**\n * Object describing changes applied to dashboard widgets that can be easily mapped to DashboardHistoryDescription widgetChanges.\n * ```ts\n * {\n * removed: {\n * 0969692617637703: { componentId: \"Data points graph\", config: {...}, classes: {...} ...}\n * },\n * config: {\n * 6347567345767653: { componentId: \"Applications\", config: {...}, classes: {...} ...},\n * 6456345634564566: { componentId: \"Events list\", config: {...}, classes: {...} ...},\n * }\n * }\n * ```\n */\n changeHistory: Partial<\n Record<\n keyof DashboardHistoryDescription['widgetChanges'],\n {\n [id: string]: Widget;\n }\n >\n >;\n};\n\nexport enum ContextDashboardType {\n Device = 'device',\n Type = 'type',\n Group = 'group',\n Named = 'name',\n Report = 'report'\n}\n\nexport enum DashboardDetailsTabId {\n GENERAL = 'general',\n APPEARANCE = 'appearance',\n VERSIONHISTORY = 'versionHistory'\n}\n\nexport type DashboardDetailsTabs = Record<\n DashboardDetailsTabId,\n TabWithTemplate<string> & { featureId: DashboardDetailsTabId }\n>;\n\nexport interface DashboardAndWidgetThemeDefinition {\n label: string;\n class: string;\n description: string;\n}\n\nexport const WIDGET_HEADER_CLASSES = [\n {\n label: gettext('Regular`style`'),\n class: 'panel-title-regular',\n description: gettext('The widget has no border between header and content.')\n },\n {\n label: gettext('Border`style`'),\n class: 'panel-title-border',\n description: gettext('The widget has a small separation border between header and content.')\n },\n {\n label: gettext('Overlay`style`'),\n class: 'panel-title-overlay',\n description: gettext('The widget content overlays the header.')\n },\n {\n label: gettext('Hidden`style`'),\n class: 'panel-title-hidden',\n description: gettext('The widget header is not shown.')\n }\n] as const satisfies DashboardAndWidgetThemeDefinition[];\n\nexport const WIDGET_CONTENT_CLASSES = [\n {\n label: gettext('Branded`style`'),\n class: 'panel-content-branded',\n description: gettext('The widget is styled with the main brand color.')\n },\n {\n label: gettext('Match dashboard`style`'),\n class: 'panel-content-light',\n description: gettext('The widget appearance matches the dashboard appearance.')\n },\n {\n label: gettext('Light`style`'),\n class: 'panel-content-white',\n description: gettext('The widget has light appearance, that is, dark text on light background.')\n },\n {\n label: gettext('Dark`style`'),\n class: 'panel-content-dark',\n description: gettext('The widget has dark appearance, that is, light text on dark background.')\n },\n {\n label: gettext('Transparent`style`'),\n class: 'panel-content-transparent',\n description: gettext('The widget has no background.')\n }\n] as const satisfies DashboardAndWidgetThemeDefinition[];\n\nexport const DASHBOARD_THEME_CLASSES = [\n {\n label: gettext('Match UI`theme`'),\n class: 'dashboard-theme-light',\n description: gettext('The dashboard appearance matches the UI appearance.')\n },\n {\n label: gettext('Light`theme`'),\n class: 'dashboard-theme-white',\n description: gettext(\n 'The dashboard has light appearance, that is, dark text on light background.'\n )\n },\n {\n label: gettext('Dark`theme`'),\n class: 'dashboard-theme-dark',\n description: gettext(\n 'The dashboard has dark appearance, that is, light text on dark background.'\n )\n },\n {\n label: gettext('Branded`theme`'),\n class: 'dashboard-theme-branded',\n description: gettext('The dashboard is styled using the brand palette.')\n }\n] as const satisfies DashboardAndWidgetThemeDefinition[];\n\nexport const STYLING_CLASS_PREFIXES = [\n 'dashboard-theme-',\n 'panel-title-',\n 'panel-content-'\n] as const;\n\nexport interface DashboardCopyClipboard {\n dashboardId: string;\n dashboard: ContextDashboard;\n context: DashboardContext;\n}\n\nexport interface DashboardContext {\n context: ViewContext;\n contextData: Partial<IManagedObject>;\n}\n\nexport const ALL_GLOBAL_ROLES_SELECTED = 'all' as const;\nexport type DashboardGlobalRoles = number[] | typeof ALL_GLOBAL_ROLES_SELECTED;\n\nexport const PRODUCT_EXPERIENCE = {\n DASHBOARD: {\n EVENTS: {\n DASHBOARDS: 'dashboards',\n REPORTS: 'reports',\n DASHBOARD_TEMPLATE: 'dashboardTemplate'\n },\n COMPONENTS: {\n DASHBOARD_VIEW: 'context-dashboard',\n DASHBOARD_AVAILABILITY: 'dashboard-availability',\n REPORTS_LIST: 'report-dashboard-list',\n ADD_REPORT: 'report-dashboard-list',\n ADD_DASHBOARD: 'add-dashboard',\n DELETE_DASHBOARD: 'context-dashboard',\n TYPED_DASHBOARD_SETTINGS: 'typed-dashboard-settings'\n },\n CONTEXT: {\n REPORT: 'report',\n DEVICE: 'device',\n ASSET: 'asset',\n GROUP: 'group'\n },\n ACTIONS: {\n APPLY_GLOBAL_ROLES_CHANGES: 'applyGlobalRolesChanges',\n DELETE: 'delete',\n LOAD: 'load',\n CREATE: 'create',\n ADD_REPORT: 'addReport',\n DUPLICATE_AS_REGULAR_DASHBOARD: 'duplicateAsRegularDashboard'\n }\n }\n} as const;\n\nexport interface CanDeactivateComponent {\n canDeactivate: () => boolean | Observable<boolean> | Promise<boolean>;\n}\n\nexport const REPORT_DEFAULT_NAVIGATION_NODE_PRIORITY = 30;\n\nexport type AllowTypeDashboard = 'allow' | 'disallow' | 'allow_if_type_filled';\n\nexport const DASHBOARD_DETAILS_OUTLET = 'dashboard-details' as const;\nexport const DASHBOARD_DETAILS_TABS_OUTLET_NAME = 'dashboardTabs' as const;\n\nexport interface DashboardMetadata {\n isReport: boolean;\n isNamedDashboard: boolean;\n hideAvailability: boolean;\n dashboard: ContextDashboard;\n deviceTypeValue: string;\n displayDeviceTypeValue: string;\n mo: ContextDashboardManagedObject;\n allowTypeDashboard: AllowTypeDashboard;\n isDevice: boolean;\n context: any;\n}\n","import { Injectable } from '@angular/core';\nimport { ActivatedRouteSnapshot, Router } from '@angular/router';\nimport { IManagedObject, InventoryService, IResultList, QueriesUtil } from '@c8y/client';\nimport {\n AlertService,\n ApplicationOptions,\n AppStateService,\n ContextData,\n ContextRouteService,\n DynamicComponentService,\n getActivatedRoute,\n GroupService,\n ModalService,\n NavigatorNode,\n NavigatorService,\n NEW_DASHBOARD_ROUTER_STATE_PROP,\n OptionsService,\n Permissions,\n Status,\n Tab,\n TabsService,\n ViewContext,\n Widget\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\nimport {\n assign,\n cloneDeep,\n forEach,\n get,\n has,\n isEmpty,\n keyBy,\n keys,\n pick,\n reduce,\n set,\n some\n} from 'lodash-es';\nimport { combineLatest, from, Observable, of, Subject } from 'rxjs';\nimport { catchError, filter, map, mergeMap, tap, throwIfEmpty, toArray } from 'rxjs/operators';\nimport {\n ALL_GLOBAL_ROLES_SELECTED,\n AllowTypeDashboard,\n ContextDashboard,\n ContextDashboardManagedObject,\n ContextDashboardType,\n DashboardContext,\n DashboardCopyClipboard,\n PRODUCT_EXPERIENCE,\n STYLING_CLASS_PREFIXES\n} from './context-dashboard.model';\n\n@Injectable({ providedIn: 'root' })\nexport class ContextDashboardService {\n dashboardTabs$: Observable<Tab[]>;\n formDisabled$: Observable<boolean>;\n readonly REPORT_PARTIAL_NAME = 'report_';\n copyClipboard: DashboardCopyClipboard;\n queriesUtil: QueriesUtil;\n readonly VERSION_HISTORY_SIZE_LIMIT = 10;\n private readonly INVENTORY_ROLES = [\n Permissions.ROLE_INVENTORY_ADMIN,\n Permissions.ROLE_MANAGED_OBJECT_ADMIN\n ];\n private cache = new Map<string, ContextDashboardManagedObject>();\n private readonly DEFAULT_PAGESIZE = 1000;\n private readonly FRAGMENT_NAME = 'c8y_Dashboard';\n private readonly APPLIED_TO_FRAGMENT = 'c8y_AppliedToApplications';\n private readonly DASHBOARD_ROUTE_PATH = 'dashboard';\n private readonly INDEX_SPLIT = '!';\n private readonly CACHE_TIMEOUT = 500;\n private _formDisabled = true;\n private formDisabledSubject = new Subject<boolean>();\n private contextDashboardsCache: {\n query: string;\n result: Promise<IResultList<IManagedObject>>;\n timestamp: number;\n };\n private appName = '';\n private readonly HIDE_TYPE_DASHBOARD_FOR_ASSETS: keyof ApplicationOptions =\n 'hideTypeDashboardForAssets';\n\n get formDisabled() {\n return this._formDisabled;\n }\n\n set formDisabled(value) {\n this._formDisabled = value;\n this.formDisabledSubject.next(value);\n }\n\n constructor(\n private inventory: InventoryService,\n private tabs: TabsService,\n private modal: ModalService,\n private translateService: TranslateService,\n private router: Router,\n private navigator: NavigatorService,\n private permissions: Permissions,\n private alert: AlertService,\n private dynamicComponent: DynamicComponentService,\n private groupService: GroupService,\n private optionsService: OptionsService,\n private appStateService: AppStateService,\n private contextRouteService: ContextRouteService\n ) {\n this.formDisabled$ = this.formDisabledSubject.asObservable();\n this.queriesUtil = new QueriesUtil();\n }\n\n async create(dashboardCfg: ContextDashboard, context?: ContextData, name = '') {\n let dashboard: Partial<ContextDashboardManagedObject> = {};\n assign(\n dashboard,\n this.adjustDashboardFor24Columns({ c8y_Dashboard: dashboardCfg }),\n this.updateDashboardHistory(dashboard, dashboardCfg)\n );\n\n const [dashboardType, dashboardFragments] = this.getDashboardFragments(\n { c8y_Dashboard: dashboardCfg } as ContextDashboardManagedObject,\n context,\n name,\n false\n );\n dashboard = { ...dashboard, ...dashboardFragments };\n\n if (this.shouldSetGlobal(dashboard, context)) {\n assign(dashboard, { c8y_Global: {} });\n }\n dashboard.name = dashboard.c8y_Dashboard.name;\n if (this.appStateService?.currentApplication?.value) {\n dashboard[this.APPLIED_TO_FRAGMENT] = [this.appStateService?.currentApplication?.value.key];\n }\n\n const { data } =\n dashboardType === ContextDashboardType.Group ||\n dashboardType === ContextDashboardType.Device ||\n (context?.contextData?.id && dashboardType === ContextDashboardType.Named)\n ? await this.inventory.childAdditionsCreate(\n dashboard,\n (context?.contextData.id as string) || ''\n )\n : await this.inventory.create(dashboard);\n return data as ContextDashboardManagedObject;\n }\n\n async detail(dashboardMO: ContextDashboardManagedObject) {\n let { data } = await this.inventory.detail(dashboardMO);\n data = this.adjustDashboardFor24Columns(data as ContextDashboardManagedObject);\n this.cache.set(dashboardMO.id, data);\n return data;\n }\n\n async update(\n dashboard: ContextDashboardManagedObject,\n context?: ContextData\n ): Promise<ContextDashboardManagedObject> {\n const dashboardCfg = dashboard.c8y_Dashboard;\n dashboard.name = dashboard.c8y_Dashboard.name;\n assign(\n dashboard,\n this.adjustDashboardFor24Columns({ c8y_Dashboard: dashboardCfg }),\n this.updateDashboardHistory(dashboard, dashboardCfg)\n );\n\n const keepFragments = this.clean(\n pick(dashboard, [this.FRAGMENT_NAME, 'id', 'name', this.APPLIED_TO_FRAGMENT])\n );\n keepFragments.c8y_DashboardHistory = dashboard.c8y_DashboardHistory;\n await this.serializeWidgetConfigs(keepFragments);\n\n const [, dashboardTypeFragments] = this.getDashboardFragments(dashboard, context, '', true);\n keepFragments.c8y_Global = this.shouldSetGlobal({ ...dashboard, ...dashboardTypeFragments });\n const { data } = await this.inventory.update({ ...keepFragments, ...dashboardTypeFragments });\n this.cache.set(dashboard.id, data);\n return data;\n }\n\n async delete(dashboard: ContextDashboardManagedObject, withConfirmation = true) {\n try {\n if (withConfirmation) {\n let msg: string = gettext(\n `You are about to delete the dashboard \"{{ dashboardName }}\". Do you want to proceed?`\n );\n if (this.isDeviceType(dashboard)) {\n msg = gettext(\n `You are about to delete the dashboard \"{{ dashboardName }}\" from all devices of the type \"{{ deviceType }}\".\n Do you want to proceed?`\n );\n }\n await this.modal.confirm(\n gettext('Delete dashboard'),\n this.translateService.instant(msg, {\n dashboardName: dashboard.c8y_Dashboard.name,\n deviceType: dashboard.c8y_Dashboard.deviceTypeValue\n }),\n Status.DANGER,\n { ok: gettext('Delete'), cancel: gettext('Cancel') }\n );\n }\n await this.inventory.delete(dashboard);\n const tabToRemove = Array.from(this.tabs.state).find(tab => {\n if (typeof tab.path === 'string') {\n return tab.path.endsWith(`${this.DASHBOARD_ROUTE_PATH}/${dashboard.id}`);\n }\n });\n this.tabs.remove(tabToRemove);\n queueMicrotask(() => {\n this.tabs.refresh();\n });\n } catch (ex) {\n // intended empty\n }\n }\n\n updateDashboardHistory(\n dashboard: Partial<ContextDashboardManagedObject>,\n dashboardCfg: ContextDashboard\n ): Partial<ContextDashboardManagedObject> {\n if (!dashboard.c8y_DashboardHistory) {\n dashboard.c8y_DashboardHistory = [];\n }\n if (isEmpty(dashboardCfg?.historyDescription)) {\n dashboardCfg.historyDescription = { changeType: 'create' };\n }\n\n dashboardCfg.created = new Date().toISOString();\n dashboard.c8y_DashboardHistory = cloneDeep([dashboardCfg, ...dashboard.c8y_DashboardHistory]);\n\n if (dashboard.c8y_DashboardHistory.length > this.VERSION_HISTORY_SIZE_LIMIT) {\n dashboard.c8y_DashboardHistory = [\n ...dashboard.c8y_DashboardHistory.slice(0, this.VERSION_HISTORY_SIZE_LIMIT)\n ];\n }\n\n return dashboard;\n }\n\n activateDashboards(\n route: ActivatedRouteSnapshot,\n types: ContextDashboardType[]\n ): Observable<boolean | Tab[]> {\n const { dashboardId } = route.params;\n if (dashboardId) {\n return this.getDashboard$(dashboardId, types, route.parent.data.contextData).pipe(\n tap(dashboard => {\n route.data = { dashboard };\n }),\n map(() => true),\n catchError(() => {\n return of(false);\n })\n );\n }\n\n const { contextData: mo } = this.contextRouteService.getContextData(route);\n\n this.dashboardTabs$ = this.getTabs$(\n mo as ContextDashboardManagedObject,\n types,\n route?.parent?.data as ContextData\n );\n return this.dashboardTabs$;\n }\n\n getDashboard(name: string, defaultWidgets: Widget[]) {\n const children = this.mapWidgets(defaultWidgets);\n return this.getDashboard$(name, [ContextDashboardType.Named]).pipe(\n throwIfEmpty(),\n catchError(() => {\n if (!this.hasPermissionsToCopyDashboard()) {\n this.alert.warning(\n gettext(\n 'You are viewing a read-only dashboard because you don’t have the necessary permissions to modify it.'\n )\n );\n }\n\n return of(\n this.getDefaultDashboard({\n name,\n children,\n widgetClasses: { 'dashboard-theme-light': true, 'panel-title-regular': true }\n })\n );\n })\n );\n }\n\n updateNavigatorItem(mo: IManagedObject) {\n this.navigator.state.forEach(node => {\n if (node.path === `reports/${mo.id}`) {\n this.navigator.remove(node);\n }\n });\n if (mo.c8y_IsNavigatorNode) {\n const nodeToAdd = new NavigatorNode({\n label: mo.name,\n path: `reports/${mo.id}`,\n icon: mo.icon,\n priority: mo.priority,\n translateLabel: mo.translateDashboardTitle\n });\n this.navigator.add(nodeToAdd);\n }\n }\n\n async navigateToDashboard(dashboardMO: ContextDashboardManagedObject, isNewDashboard = false) {\n if (/\\/dashboard\\//.test(this.router.url)) {\n this.router.navigate(['..', dashboardMO.id], {\n relativeTo: getActivatedRoute(this.router),\n ...(isNewDashboard && {\n state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }\n })\n });\n } else if (/^\\/(device|group)\\/[0-9]+$/.test(this.router.url)) {\n // in case the add dashboard button is the only tab on that route\n this.router.navigate(['.', this.DASHBOARD_ROUTE_PATH, dashboardMO.id], {\n relativeTo: getActivatedRoute(this.router),\n ...(isNewDashboard && {\n state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }\n })\n });\n } else if (/^\\/(device|group)\\/[0-9]+\\/device-info$/.test(this.router.url)) {\n this.router.navigate(['.'], {\n relativeTo: getActivatedRoute(this.router),\n ...(isNewDashboard && {\n state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }\n })\n });\n } else {\n this.router.navigate(['..', this.DASHBOARD_ROUTE_PATH, dashboardMO.id], {\n relativeTo: getActivatedRoute(this.router),\n ...(isNewDashboard && {\n state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }\n })\n });\n }\n }\n\n /**\n * Checks if user is able to edit dashboard according to his roles and dashboard ownership.\n *\n * @param mo - Dashboard managed object.\n * @returns True if user is able to edit dashboard, false if he cannot.\n */\n async canEditDashboard(mo: ContextDashboardManagedObject): Promise<boolean> {\n return await this.permissions.canEdit(this.INVENTORY_ROLES, mo);\n }\n\n /**\n * Checks if user has permissions to copy dashboard according to his roles.\n *\n * @returns True if user has permissions to copy dashboard, false if he cannot.\n */\n hasPermissionsToCopyDashboard(): boolean {\n return this.permissions.hasAnyRole([\n Permissions.ROLE_INVENTORY_ADMIN,\n Permissions.ROLE_INVENTORY_CREATE,\n Permissions.ROLE_MANAGED_OBJECT_ADMIN,\n Permissions.ROLE_MANAGED_OBJECT_CREATE\n ]);\n }\n\n isNamed(dashboard: Partial<ContextDashboardManagedObject>) {\n return some(keys(dashboard), prop =>\n new RegExp(\n `^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}`\n ).test(prop)\n );\n }\n\n isReport(dashboard: Partial<ContextDashboardManagedObject>) {\n return some(keys(dashboard), prop =>\n new RegExp(\n `^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}${this.REPORT_PARTIAL_NAME}`\n ).test(prop)\n );\n }\n\n isDeviceType(dashboard: Partial<ContextDashboardManagedObject>) {\n return some(keys(dashboard), prop => {\n const matchingProp = new RegExp(\n `^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Type}${this.INDEX_SPLIT}`\n ).test(prop);\n if (!matchingProp) {\n return false;\n } else {\n // there might be matching key, but its value can be {} or null\n return !!dashboard[prop];\n }\n });\n }\n\n isDeviceDashboard(dashboard: Partial<ContextDashboardManagedObject>): boolean {\n return some(keys(dashboard), prop =>\n new RegExp(\n `^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Device}${this.INDEX_SPLIT}`\n ).test(prop)\n );\n }\n\n isGroupDashboard(dashboard: Partial<ContextDashboardManagedObject>): boolean {\n return some(keys(dashboard), prop =>\n new RegExp(\n `^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Group}${this.INDEX_SPLIT}`\n ).test(prop)\n );\n }\n\n getFilteredDashboardStyles(styleList: string[]) {\n return styleList.filter(c =>\n STYLING_CLASS_PREFIXES.some(classPrefix => c.startsWith(classPrefix))\n );\n }\n\n getStyling(styleList, styleName, defaultValue) {\n const styling = styleList.find(\n style => style && new RegExp(`-${styleName}$`, 'i').test(style.class)\n );\n return styling ? styling.class : defaultValue;\n }\n\n mapWidgets(widgets: Widget[]) {\n return keyBy(\n widgets.map(widget => {\n widget.id = String(Math.random()).substr(2);\n return widget;\n }),\n 'id'\n );\n }\n\n getDashboard$(dashboardIdOrName, dashboardType: ContextDashboardType[], mo?: IManagedObject) {\n const cache = this.cache.get(dashboardIdOrName);\n\n const dashboards = mo\n ? this.getContextDashboards(mo, dashboardType)\n : this.getNamedDashboard(dashboardIdOrName);\n\n const cacheRefresh = this.getContextDashboards$(dashboards).pipe(\n tap(dashboard => this.cacheDashboard(dashboard)),\n filter(\n dashboard =>\n dashboard.id === dashboardIdOrName ||\n has(\n dashboard,\n `${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}${dashboardIdOrName}`\n )\n )\n );\n return cache ? of(cache) : cacheRefresh;\n }\n\n async pasteDashboard(newContext: DashboardContext): Promise<void> {\n if (this.copyClipboard) {\n try {\n const dashboardToPaste = this.createContextDashboardCopy(\n this.copyClipboard.dashboard,\n newContext.contextData,\n this.copyClipboard.context.contextData\n );\n const dashboard = await this.create(this.clean(dashboardToPaste), newContext);\n\n // linking childAdditions for e.g. to grant access to the images uploaded by the image widget for users with only inventory roles.\n const { data: childAdditions } = await this.inventory.childAdditionsList(\n this.copyClipboard.dashboardId,\n { pageSize: 2000 }\n );\n if (childAdditions.length) {\n await this.inventory.childAdditionsBulkAdd(childAdditions, dashboard.id);\n }\n\n this.copyClipboard = undefined;\n this.navigateToDashboard(dashboard);\n } catch {\n this.alert.warning(gettext('Insufficient permissions for this action.'));\n }\n }\n }\n\n /**\n * Creates fragment that associates dashboards with device/asset. It consists of three elements:\n * - FRAGMENT_NAME - static string\n * - dashboard type (e.g. 'group', 'device')\n * - fragment value ( id of device/asset if it is not typed dashboard; deviceTypeValue property of dashboard if it is type dashboard)\n * Example fragment for device dashboard: 'c8y_Dashboard!device!773200'\n * Example fragment for group dashboard: 'c8y_Dashboard!group!84129208'\n * Example fragment for typed device dashboard: 'c8y_Dashboard!type!c8y_lwm2m_connector_device'\n *\n * @param contextDashboardType Type of dashboard\n * @param value Fragment value\n * @returns Fragment for dashboard\n */\n createFragmentKey<T extends ContextDashboardType, V extends string>(\n contextDashboardType: T,\n value: V\n ) {\n return `${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${contextDashboardType}${this.INDEX_SPLIT}${value}` as const;\n }\n\n /**\n * Indicates if dashboard can be set to type dashboard.\n * First, it checks if deviceTypeValue exists and if user has permission to set dashboard type.\n * Then, case from sensor app is checked- dashboard created with sensor app has deviceType set to true but\n * type fragment is missing- we do not support this combination.\n * @param mo Dashboard managed object\n * @param context {ContextData} Current context\n * @returns True if dashboard can be set to type dashboard, false if it is forbidden.\n */\n shouldAllowToSetDashboardType(\n mo: ContextDashboardManagedObject,\n context: ContextData\n ): AllowTypeDashboard {\n // disallow if dashboard managed object or context is missing or context is not device/asset/group\n if (\n !mo ||\n !context?.contextData ||\n (context.context !== ViewContext.Device && context.context !== ViewContext.Group)\n ) {\n return 'disallow';\n }\n\n // if context is asset/group and type dashboard feature is hidden for assets/groups or asset/group has no typ, return disallow\n const typeDashboardHiddenForAssets = this.optionsService.get(\n this.HIDE_TYPE_DASHBOARD_FOR_ASSETS,\n true\n );\n if (\n context.context === ViewContext.Group &&\n (typeDashboardHiddenForAssets || !context.contextData.type)\n ) {\n return 'disallow';\n }\n\n // if user has no permission to change dashboard, return disallow\n if (!this.permissions.hasAnyRole(this.INVENTORY_ROLES)) {\n return 'disallow';\n }\n\n // case from sensor app is checked- dashboard created with sensor app has deviceType set to true but\n // type fragment is missing- we do not support this combination.\n const typeFragment = this.createFragmentKey(\n ContextDashboardType.Type,\n context?.contextData?.type\n );\n if (\n mo?.c8y_Dashboard &&\n mo?.c8y_Dashboard.deviceType &&\n context?.contextData?.type &&\n !mo[typeFragment]\n ) {\n return 'disallow';\n }\n\n // if view context is Device and contextData of this device has no type yet but type dashboard can be set when type is filled,\n // return allow_if_type_filled\n if (\n !context?.contextData?.type &&\n context.context === ViewContext.Device &&\n this.permissions.hasAnyRole(this.INVENTORY_ROLES)\n ) {\n return 'allow_if_type_filled';\n }\n\n return 'allow';\n }\n\n createReport(reportCfg: Partial<IManagedObject>) {\n const report: Partial<IManagedObject> = {};\n Object.assign(report, reportCfg);\n Object.assign(report, { c8y_Report: {} });\n return this.inventory.create(report);\n }\n\n addReportNavigatorNode(report: IManagedObject): void {\n const node = new NavigatorNode({\n label: report.name,\n path: `reports/${report.id}`,\n icon: report.icon,\n priority: report.priority,\n translateLabel: report.translateDashboardTitle\n });\n this.navigator.add(node);\n }\n\n getContextForGS(mo: IManagedObject): string | null {\n if (this.groupService.isDevice(mo)) {\n return PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.DEVICE;\n } else if (this.groupService.isAsset(mo)) {\n return PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.ASSET;\n } else if (this.groupService.isGroup(mo)) {\n return PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.GROUP;\n } else {\n return null;\n }\n }\n\n async getContextDashboards(\n mo: IManagedObject,\n dashboardType: ContextDashboardType[]\n ): Promise<IResultList<IManagedObject>> {\n const filterCriteria = dashboardType.map(t => ({\n // it's necessary to wrap fragment in quotes because dashboard type can contain spaces\n __has: `'${this.createDashboardFragment(mo, t)}'`\n }));\n\n // the has query above does not work for device type dashboards where the type contains a dot\n const typeFilterCriteria =\n dashboardType.includes(ContextDashboardType.Type) && mo.type\n ? {\n __and: [\n { 'c8y_Dashboard.deviceType': { __eq: true } },\n { 'c8y_Dashboard.deviceTypeValue': { __eq: mo.type } }\n ]\n }\n : undefined;\n const finalFilterCriteria = typeFilterCriteria\n ? [...filterCriteria, typeFilterCriteria]\n : filterCriteria;\n\n const query = this.queriesUtil.buildQuery({\n __filter: {\n __and: [{ __or: finalFilterCriteria }, this.appliedToFilter()]\n }\n });\n\n const now = Date.now();\n const cacheHasValidResponse =\n this.contextDashboardsCache &&\n this.contextDashboardsCache.query === query &&\n now - this.contextDashboardsCache.timestamp < this.CACHE_TIMEOUT;\n\n if (cacheHasValidResponse) {\n return this.contextDashboardsCache.result;\n } else {\n this.contextDashboardsCache = null;\n }\n\n this.contextDashboardsCache = {\n query,\n result: this.inventory.list({ query, pageSize: this.DEFAULT_PAGESIZE }),\n timestamp: now\n };\n return this.contextDashboardsCache.result;\n }\n\n appliedToFilter() {\n const key = this.appStateService?.currentApplication?.value?.key || '';\n\n if (!key) {\n return {};\n }\n\n return {\n __or: [\n { __not: { __has: this.APPLIED_TO_FRAGMENT } }, // legacy / unlabeled ⇒ show in all\n { [this.APPLIED_TO_FRAGMENT]: { __in: [key] } }\n ]\n };\n }\n\n /**\n * Creates a tuple describing the dashboard type and its fragments. For assets like devices and groups, it's possible\n * to have two fragments: one indicating this particular device/asset with its ID, and the second indicating\n * the device/asset type (if the dashboard is meant to be applied to all assets of this type).\n *\n * @param dashboardMO - Dashboard managed object.\n * @param context - Context data of asset.\n * @param name - Name of the dashboard.\n * @param isEdit - True if existing dashboard is updated, false when it's creation of new dashboard.\n * @returns Tuple of dashboard type and object containing dashboard fragments.\n */\n private getDashboardFragments(\n dashboardMO: ContextDashboardManagedObject,\n context: ContextData,\n name: string,\n isEdit: boolean\n ): [ContextDashboardType, Record<string, object>] {\n let dashboardType: ContextDashboardType;\n const id = (context?.contextData?.id as string) || '';\n const fragments: Record<string, object> = {};\n\n if (name) {\n // a named dashboard should not receive any other fragments\n dashboardType = ContextDashboardType.Named;\n const namedFragmentKey = this.createFragmentKey(ContextDashboardType.Named, name);\n fragments[namedFragmentKey] = {};\n } else if (context?.context === ViewContext.Device || context?.context === ViewContext.Group) {\n // get base type for device or group\n const defaultType =\n context.context === ViewContext.Device\n ? ContextDashboardType.Device\n : ContextDashboardType.Group;\n dashboardType = dashboardMO.c8y_Dashboard.deviceType\n ? ContextDashboardType.Type\n : defaultType;\n\n // clear fragments from other asset if current asset is not origin of this dashboard\n this.clearRedundantFragment(dashboardMO, defaultType, fragments);\n\n // add base fragment for particular asset\n const deviceOrGroupFragmentKey = this.createFragmentKey(defaultType, id);\n fragments[deviceOrGroupFragmentKey] = {};\n\n // add or clear type fragment\n if (dashboardMO.c8y_Dashboard.deviceType || isEdit) {\n const typeFragmentKey = this.createFragmentKey(\n ContextDashboardType.Type,\n dashboardMO.c8y_Dashboard.deviceTypeValue\n );\n fragments[typeFragmentKey] = dashboardMO.c8y_Dashboard.deviceType ? {} : null;\n }\n }\n\n return [dashboardType, fragments];\n }\n\n /**\n * Clears fragments that originates from other managed object.\n * E.g. typed dashboard is created for device A of type c8y_MQTTDevice and id 1, so it gets fragments object\n * ```ts\n * {\n * c8y_Dashboard!device!1: {},\n * c8y_Dashboard!type!c8y_MQTTDevice: {}\n * }\n *```\n * then, on device B of type c8y_MQTTDevice and id 2, which also has access to this dashboard, deviceType is set to\n * false, so dashboard is not typed dashboard anymore and now belongs to device B, therefore fragments should look like\n * ```ts\n * {\n * c8y_Dashboard!device!1: null, // this value is cleared because dashboard is doesn't belong to device A anymore\n * c8y_Dashboard!device!2: {}, // assign dashboard to device B\n * c8y_Dashboard!type!c8y_MQTTDevice: null // this value is cleared in getDashboardFragments method as it's not typed dashboard anymore\n * }\n * ```\n *\n * @param dashboardMO - Dashboard managed object.\n * @param type - Context dashboard type.\n * @param fragments - Fragments object.\n */\n private clearRedundantFragment(\n dashboardMO: ContextDashboardManagedObject,\n type: ContextDashboardType,\n fragments: Record<string, object>\n ): void {\n Object.keys(dashboardMO)\n .filter(key => key.startsWith(this.createFragmentKey(type, '')))\n .forEach(key => (fragments[key] = null));\n }\n\n /**\n * Used to migrate dashboards from previous 12 columns layout to 24 columns.\n */\n private adjustDashboardFor24Columns<T extends { c8y_Dashboard?: ContextDashboard }>(\n dashboards: T[]\n ): T[];\n private adjustDashboardFor24Columns<T extends { c8y_Dashboard?: ContextDashboard }>(\n dashboards: T\n ): T;\n private adjustDashboardFor24Columns<T extends { c8y_Dashboard?: ContextDashboard }>(\n dashboards: T | T[]\n ): T | T[] {\n if (Array.isArray(dashboards)) {\n return dashboards.map(dashboard => this.adjustDashboardFor24Columns(dashboard));\n }\n // if `columns` attribute exists, dashboard was already adjusted.\n if (dashboards.c8y_Dashboard.columns) {\n return dashboards;\n }\n\n dashboards.c8y_Dashboard.columns = 24;\n if (!dashboards.c8y_Dashboard.children) {\n return dashboards;\n }\n\n // Newly created NamedContextDashboards are still created with 12 columns for backwards compatibility.\n // Default widgets might be already configured for 24 columns.\n // If a widget is already configured for more than 12 columns, we should not adjust it.\n const alreadyHasWidgetsConfiguredForMoreThan12Columns = Object.values(\n dashboards.c8y_Dashboard.children\n ).some(widget => widget._x + widget._width > 12);\n if (alreadyHasWidgetsConfiguredForMoreThan12Columns) {\n return dashboards;\n }\n\n // we need to multiply both _width and _x attributes with 2 to migrate from 12 to 24 columns.\n Object.values(dashboards.c8y_Dashboard.children).forEach(widget => {\n if (widget._width) {\n widget._width = widget._width * 2;\n }\n if (widget._x) {\n widget._x = widget._x * 2;\n }\n });\n return dashboards;\n }\n\n private async serializeWidgetConfigs(dashboard: ContextDashboardManagedObject): Promise<void> {\n const children = cloneDeep(dashboard.c8y_Dashboard.children);\n if (!children) {\n return;\n }\n const configs = Object.values(children);\n const details = configs.map(({ componentId, config }) => ({ componentId, config }));\n const results = await this.dynamicComponent.serializeConfigs(details);\n results.forEach((result, index) => {\n Object.entries(result).forEach(([key, value]) => {\n set(details[index].config, key, value);\n });\n });\n dashboard.c8y_Dashboard.children = children;\n }\n\n private createContextDashboardCopy(\n dash: ContextDashboard,\n newContext: Partial<IManagedObject>,\n oldContext: Partial<IManagedObject>\n ): ContextDashboard {\n const children = reduce(\n dash.children,\n (_children, child) => {\n const { id } = child;\n const cfg = child.config;\n const propertiesToCopy = {\n device: device => t