@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
4,184 lines • 260 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Component, inject, InjectionToken, EventEmitter, Output, Input, TemplateRef, ViewChild, Optional, Inject, HostListener, HostBinding, NgModule } from '@angular/core';
import * as i2 from '@c8y/ngx-components';
import { gettext, IconDirective, C8yTranslatePipe, Permissions, ContextRouteService, Status, NavigatorNode, NEW_DASHBOARD_ROUTER_STATE_PROP, getActivatedRoute, ViewContext, TabsOutletComponent, memoize, hookGeneric, ExtensionPointForPlugins, fromTriggerOnce, getInjectedHooks, stateToFactory, ListGroupModule, C8yComponentOutlet, AlertService, DynamicComponentModule, DynamicComponentComponent, sortByPriority, isPromise, CoreModule, C8yTranslateDirective, HighlightComponent, EmptyStateComponent, FormGroupComponent, RequiredInputPlaceholderDirective, ProductExperienceDirective, DashboardChildChange, CopyDashboardDisabledReason, TitleComponent, ActionBarItemComponent, HelpComponent, WidgetsDashboardComponent, DatePipe, hookRoute, hookTab, hookNavigator } from '@c8y/ngx-components';
import * as i1 from '@angular/router';
import { RouterOutlet, ActivatedRoute, RouterModule } from '@angular/router';
import { of, Subject, from, combineLatest, map as map$1, timer, BehaviorSubject, tap as tap$1, withLatestFrom, mergeMap as mergeMap$1, shareReplay as shareReplay$1, merge, scan, debounceTime, isObservable, iif } from 'rxjs';
import { __decorate, __metadata } from 'tslib';
import * as i2$1 from '@ngx-translate/core';
import { assign, pick, isEmpty, cloneDeep, some, keys, keyBy, has, set, reduce, forEach, get, isEqual, clone, omit, sortBy, escapeRegExp, findIndex, kebabCase, every } from 'lodash-es';
import * as i1$1 from '@c8y/client';
import { QueriesUtil, InventoryService } from '@c8y/client';
import { tap, map, catchError, throwIfEmpty, filter, mergeMap, toArray, first, take, distinctUntilChanged, takeUntil, shareReplay, switchMap } from 'rxjs/operators';
import * as i5 from '@angular/forms';
import { Validators, NgForm, ControlContainer, FormsModule } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import * as i2$3 from 'ngx-bootstrap/collapse';
import { CollapseDirective, CollapseModule } from 'ngx-bootstrap/collapse';
import * as i3 from '@angular/common';
import { NgIf, NgTemplateOutlet, CommonModule, AsyncPipe, NgClass, NgFor, NgForOf } from '@angular/common';
import * as i1$2 from '@c8y/ngx-components/assets-navigator';
import { AssetSelectorModule } from '@c8y/ngx-components/assets-navigator';
import * as i2$2 from 'ngx-bootstrap/popover';
import { PopoverModule, PopoverDirective } from 'ngx-bootstrap/popover';
import { IconSelectorModule } from '@c8y/ngx-components/icon-selector';
import * as i1$3 from 'ngx-bootstrap/dropdown';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { defaultWidgetIds } from '@c8y/ngx-components/widgets/definitions';
const newDashboardTab = {
featureId: 'newDashboard',
icon: 'th',
label: gettext('New dashboard'),
path: 'new-dashboard',
// place tab as last one and hide it so it won't be opened until user initiates adding new dashboard
hide: true,
priority: -Infinity
};
class NewDashboardGuard {
canActivate(route) {
const tabActive = route.routeConfig.path === newDashboardTab.path;
if (tabActive) {
const dashboard = {
c8y_Dashboard: null
};
route.data = { dashboard };
}
if (!this.tab) {
this.tab = {
...newDashboardTab,
hide: !tabActive,
priority: tabActive ? Infinity : -Infinity
};
}
return of([this.tab]);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NewDashboardGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NewDashboardGuard, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NewDashboardGuard, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class AddDashboardComponent {
constructor(tabsService, router) {
this.tabsService = tabsService;
this.router = router;
}
addDashboard() {
const tempNewDashboardTab = [...this.tabsService.state].find(t => t.featureId === newDashboardTab.featureId);
// navigate before tab is displayed, because in DashboardDetailComponent tab is hidden on navigation from it.
this.router.navigate(typeof tempNewDashboardTab.path === 'string'
? [tempNewDashboardTab.path]
: tempNewDashboardTab.path, { replaceUrl: true });
// show tab and make it appear as first one
tempNewDashboardTab.hide = false;
tempNewDashboardTab.priority = Infinity;
this.tabsService.refresh();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AddDashboardComponent, deps: [{ token: i2.TabsService }, { token: i1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: AddDashboardComponent, isStandalone: true, selector: "[c8y-add-dashboard]", host: { classAttribute: "d-flex a-i-stretch sticky-right" }, ngImport: i0, template: "<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", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AddDashboardComponent, decorators: [{
type: Component,
args: [{ selector: '[c8y-add-dashboard]', host: { class: 'd-flex a-i-stretch sticky-right' }, imports: [IconDirective, C8yTranslatePipe], template: "<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" }]
}], ctorParameters: () => [{ type: i2.TabsService }, { type: i1.Router }] });
class AddDashboardFactory {
constructor() {
this.permissions = inject(Permissions);
this.contextRoute = inject(ContextRouteService);
}
async get(activatedRoute) {
this.currentContext = this.contextRoute.getContextData(activatedRoute);
if (this.currentContext?.context === this.targetContext &&
(await this.hasPermission(this.currentContext.contextData))) {
return [
{
component: AddDashboardComponent,
priority: -Infinity,
showAlways: true
}
];
}
return [];
}
async hasPermission(context) {
if (context?.id) {
return await this.permissions.canEdit([
Permissions.ROLE_INVENTORY_ADMIN,
Permissions.ROLE_INVENTORY_CREATE,
Permissions.ROLE_MANAGED_OBJECT_ADMIN,
Permissions.ROLE_MANAGED_OBJECT_CREATE
], context);
}
return this.permissions.hasAnyRole([
Permissions.ROLE_INVENTORY_ADMIN,
Permissions.ROLE_INVENTORY_CREATE,
Permissions.ROLE_MANAGED_OBJECT_ADMIN,
Permissions.ROLE_MANAGED_OBJECT_CREATE
]);
}
}
const CONTEXT_DASHBOARD_CONFIG = new InjectionToken('ContextDashboardConfig');
const DASHBOARD_SETTINGS_CHANGES = {
classes: gettext('theme'),
globalRolesIds: gettext('global roles'),
widgetClasses: gettext('widget header style'),
widgetMargin: gettext('widget margin'),
icon: gettext('icon'),
name: gettext('name'),
priority: gettext('priority'),
c8y_IsNavigatorNode: gettext('navigator item'),
translateWidgetTitle: gettext('translate widget title'),
children: gettext('widgets')
};
const DASHBOARD_CHILDREN_STATE_NAME = {
initial: gettext('Initial state'),
config: gettext('Widget configuration changed'),
removed: gettext('Widget removed'),
added: gettext('Widget added'),
arrangement: gettext('Widgets rearranged')
};
var ContextDashboardType;
(function (ContextDashboardType) {
ContextDashboardType["Device"] = "device";
ContextDashboardType["Type"] = "type";
ContextDashboardType["Group"] = "group";
ContextDashboardType["Named"] = "name";
ContextDashboardType["Report"] = "report";
})(ContextDashboardType || (ContextDashboardType = {}));
var DashboardDetailsTabId;
(function (DashboardDetailsTabId) {
DashboardDetailsTabId["GENERAL"] = "general";
DashboardDetailsTabId["APPEARANCE"] = "appearance";
DashboardDetailsTabId["VERSIONHISTORY"] = "versionHistory";
})(DashboardDetailsTabId || (DashboardDetailsTabId = {}));
const WIDGET_HEADER_CLASSES = [
{
label: gettext('Regular`style`'),
class: 'panel-title-regular',
description: gettext('The widget has no border between header and content.')
},
{
label: gettext('Border`style`'),
class: 'panel-title-border',
description: gettext('The widget has a small separation border between header and content.')
},
{
label: gettext('Overlay`style`'),
class: 'panel-title-overlay',
description: gettext('The widget content overlays the header.')
},
{
label: gettext('Hidden`style`'),
class: 'panel-title-hidden',
description: gettext('The widget header is not shown.')
}
];
const WIDGET_CONTENT_CLASSES = [
{
label: gettext('Branded`style`'),
class: 'panel-content-branded',
description: gettext('The widget is styled with the main brand color.')
},
{
label: gettext('Match dashboard`style`'),
class: 'panel-content-light',
description: gettext('The widget appearance matches the dashboard appearance.')
},
{
label: gettext('Light`style`'),
class: 'panel-content-white',
description: gettext('The widget has light appearance, that is, dark text on light background.')
},
{
label: gettext('Dark`style`'),
class: 'panel-content-dark',
description: gettext('The widget has dark appearance, that is, light text on dark background.')
},
{
label: gettext('Transparent`style`'),
class: 'panel-content-transparent',
description: gettext('The widget has no background.')
}
];
const DASHBOARD_THEME_CLASSES = [
{
label: gettext('Match UI`theme`'),
class: 'dashboard-theme-light',
description: gettext('The dashboard appearance matches the UI appearance.')
},
{
label: gettext('Light`theme`'),
class: 'dashboard-theme-white',
description: gettext('The dashboard has light appearance, that is, dark text on light background.')
},
{
label: gettext('Dark`theme`'),
class: 'dashboard-theme-dark',
description: gettext('The dashboard has dark appearance, that is, light text on dark background.')
},
{
label: gettext('Branded`theme`'),
class: 'dashboard-theme-branded',
description: gettext('The dashboard is styled using the brand palette.')
}
];
const STYLING_CLASS_PREFIXES = [
'dashboard-theme-',
'panel-title-',
'panel-content-'
];
const ALL_GLOBAL_ROLES_SELECTED = 'all';
const PRODUCT_EXPERIENCE = {
DASHBOARD: {
EVENTS: {
DASHBOARDS: 'dashboards',
REPORTS: 'reports',
DASHBOARD_TEMPLATE: 'dashboardTemplate'
},
COMPONENTS: {
DASHBOARD_VIEW: 'context-dashboard',
DASHBOARD_AVAILABILITY: 'dashboard-availability',
REPORTS_LIST: 'report-dashboard-list',
ADD_REPORT: 'report-dashboard-list',
ADD_DASHBOARD: 'add-dashboard',
DELETE_DASHBOARD: 'context-dashboard',
TYPED_DASHBOARD_SETTINGS: 'typed-dashboard-settings'
},
CONTEXT: {
REPORT: 'report',
DEVICE: 'device',
ASSET: 'asset',
GROUP: 'group'
},
ACTIONS: {
APPLY_GLOBAL_ROLES_CHANGES: 'applyGlobalRolesChanges',
DELETE: 'delete',
LOAD: 'load',
CREATE: 'create',
ADD_REPORT: 'addReport',
DUPLICATE_AS_REGULAR_DASHBOARD: 'duplicateAsRegularDashboard'
}
}
};
const REPORT_DEFAULT_NAVIGATION_NODE_PRIORITY = 30;
const DASHBOARD_DETAILS_OUTLET = 'dashboard-details';
const DASHBOARD_DETAILS_TABS_OUTLET_NAME = 'dashboardTabs';
class ContextDashboardService {
get formDisabled() {
return this._formDisabled;
}
set formDisabled(value) {
this._formDisabled = value;
this.formDisabledSubject.next(value);
}
constructor(inventory, tabs, modal, translateService, router, navigator, permissions, alert, dynamicComponent, groupService, optionsService) {
this.inventory = inventory;
this.tabs = tabs;
this.modal = modal;
this.translateService = translateService;
this.router = router;
this.navigator = navigator;
this.permissions = permissions;
this.alert = alert;
this.dynamicComponent = dynamicComponent;
this.groupService = groupService;
this.optionsService = optionsService;
this.REPORT_PARTIAL_NAME = 'report_';
this.VERSION_HISTORY_SIZE_LIMIT = 10;
this.INVENTORY_ROLES = [
Permissions.ROLE_INVENTORY_ADMIN,
Permissions.ROLE_MANAGED_OBJECT_ADMIN
];
this.cache = new Map();
this.DEFAULT_PAGESIZE = 1000;
this.FRAGMENT_NAME = 'c8y_Dashboard';
this.DASHBOARD_ROUTE_PATH = 'dashboard';
this.INDEX_SPLIT = '!';
this.CACHE_TIMEOUT = 500;
this._formDisabled = true;
this.formDisabledSubject = new Subject();
this.HIDE_TYPE_DASHBOARD_FOR_ASSETS = 'hideTypeDashboardForAssets';
this.formDisabled$ = this.formDisabledSubject.asObservable();
this.queriesUtil = new QueriesUtil();
}
async create(dashboardCfg, context, name = '') {
let dashboard = {};
assign(dashboard, this.adjustDashboardFor24Columns({ c8y_Dashboard: dashboardCfg }), this.updateDashboardHistory(dashboard, dashboardCfg));
const [dashboardType, dashboardFragments] = this.getDashboardFragments({ c8y_Dashboard: dashboardCfg }, context, name, false);
dashboard = { ...dashboard, ...dashboardFragments };
if (this.shouldSetGlobal(dashboard, context)) {
assign(dashboard, { c8y_Global: {} });
}
dashboard.name = dashboard.c8y_Dashboard.name;
const { data } = dashboardType === ContextDashboardType.Group ||
dashboardType === ContextDashboardType.Device ||
(context?.contextData?.id && dashboardType === ContextDashboardType.Named)
? await this.inventory.childAdditionsCreate(dashboard, context?.contextData.id || '')
: await this.inventory.create(dashboard);
return data;
}
async detail(dashboardMO) {
let { data } = await this.inventory.detail(dashboardMO);
data = this.adjustDashboardFor24Columns(data);
this.cache.set(dashboardMO.id, data);
return data;
}
async update(dashboard, context) {
const dashboardCfg = dashboard.c8y_Dashboard;
dashboard.name = dashboard.c8y_Dashboard.name;
assign(dashboard, this.adjustDashboardFor24Columns({ c8y_Dashboard: dashboardCfg }), this.updateDashboardHistory(dashboard, dashboardCfg));
const keepFragments = this.clean(pick(dashboard, [this.FRAGMENT_NAME, 'id', 'name']));
keepFragments.c8y_DashboardHistory = dashboard.c8y_DashboardHistory;
await this.serializeWidgetConfigs(keepFragments);
const [, dashboardTypeFragments] = this.getDashboardFragments(dashboard, context, '', true);
keepFragments.c8y_Global = this.shouldSetGlobal({ ...dashboard, ...dashboardTypeFragments });
const { data } = await this.inventory.update({ ...keepFragments, ...dashboardTypeFragments });
this.cache.set(dashboard.id, data);
return data;
}
async delete(dashboard, withConfirmation = true) {
try {
if (withConfirmation) {
let msg = gettext(`You are about to delete the dashboard "{{ dashboardName }}". Do you want to proceed?`);
if (this.isDeviceType(dashboard)) {
msg = gettext(`You are about to delete the dashboard "{{ dashboardName }}" from all devices of the type "{{ deviceType }}".
Do you want to proceed?`);
}
await this.modal.confirm(gettext('Delete dashboard'), this.translateService.instant(msg, {
dashboardName: dashboard.c8y_Dashboard.name,
deviceType: dashboard.c8y_Dashboard.deviceTypeValue
}), Status.DANGER, { ok: gettext('Delete'), cancel: gettext('Cancel') });
}
await this.inventory.delete(dashboard);
const tabToRemove = Array.from(this.tabs.state).find(tab => {
if (typeof tab.path === 'string') {
return tab.path.endsWith(`${this.DASHBOARD_ROUTE_PATH}/${dashboard.id}`);
}
});
this.tabs.remove(tabToRemove);
queueMicrotask(() => {
this.tabs.refresh();
});
}
catch (ex) {
// intended empty
}
}
updateDashboardHistory(dashboard, dashboardCfg) {
if (!dashboard.c8y_DashboardHistory) {
dashboard.c8y_DashboardHistory = [];
}
if (isEmpty(dashboardCfg?.historyDescription)) {
dashboardCfg.historyDescription = { changeType: 'create' };
}
dashboardCfg.created = new Date().toISOString();
dashboard.c8y_DashboardHistory = cloneDeep([dashboardCfg, ...dashboard.c8y_DashboardHistory]);
if (dashboard.c8y_DashboardHistory.length > this.VERSION_HISTORY_SIZE_LIMIT) {
dashboard.c8y_DashboardHistory = [
...dashboard.c8y_DashboardHistory.slice(0, this.VERSION_HISTORY_SIZE_LIMIT)
];
}
return dashboard;
}
activateDashboards(route, types) {
const { dashboardId } = route.params;
if (dashboardId) {
return this.getDashboard$(dashboardId, types, route.parent.data.contextData).pipe(tap(dashboard => {
route.data = { dashboard };
}), map(() => true), catchError(() => {
return of(false);
}));
}
this.dashboardTabs$ = this.getTabs$(route.data.contextData, types, route?.parent?.data);
return this.dashboardTabs$;
}
getDashboard(name, defaultWidgets) {
const children = this.mapWidgets(defaultWidgets);
return this.getDashboard$(name, [ContextDashboardType.Named]).pipe(throwIfEmpty(), catchError(() => {
if (!this.hasPermissionsToCopyDashboard()) {
this.alert.warning(gettext('You are viewing a read-only dashboard because you don’t have the necessary permissions to modify it.'));
}
return of(this.getDefaultDashboard({
name,
children,
widgetClasses: { 'dashboard-theme-light': true, 'panel-title-regular': true }
}));
}));
}
/**
* @deprecated Use `getDashboard()` instead.
* This function will be removed in the future.
*/
getNamedDashboardOrCreate(name, defaultWidgets, context) {
const children = this.mapWidgets(defaultWidgets);
return this.getDashboard$(name, [ContextDashboardType.Named]).pipe(throwIfEmpty(), catchError(() => {
if (!this.hasPermissionsToCopyDashboard()) {
this.alert.warning(gettext('You are viewing a read-only dashboard because you don’t have the necessary permissions to modify it.'));
return of(this.getDefaultDashboard({
name,
children,
widgetClasses: { 'dashboard-theme-light': true, 'panel-title-regular': true }
}));
}
else
return from(this.create({
children,
widgetClasses: { 'dashboard-theme-light': true, 'panel-title-regular': true }
}, context, name));
}));
}
updateNavigatorItem(mo) {
this.navigator.state.forEach(node => {
if (node.path === `reports/${mo.id}`) {
this.navigator.remove(node);
}
});
if (mo.c8y_IsNavigatorNode) {
const nodeToAdd = new NavigatorNode({
label: mo.name,
path: `reports/${mo.id}`,
icon: mo.icon,
priority: mo.priority
});
this.navigator.add(nodeToAdd);
}
}
async navigateToDashboard(dashboardMO, isNewDashboard = false) {
if (/\/dashboard\//.test(this.router.url)) {
this.router.navigate(['..', dashboardMO.id], {
relativeTo: getActivatedRoute(this.router),
...(isNewDashboard && {
state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }
})
});
}
else if (/^\/(device|group)\/[0-9]+$/.test(this.router.url)) {
// in case the add dashboard button is the only tab on that route
this.router.navigate(['.', this.DASHBOARD_ROUTE_PATH, dashboardMO.id], {
relativeTo: getActivatedRoute(this.router),
...(isNewDashboard && {
state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }
})
});
}
else if (/^\/(device|group)\/[0-9]+\/device-info$/.test(this.router.url)) {
this.router.navigate(['.'], {
relativeTo: getActivatedRoute(this.router),
...(isNewDashboard && {
state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }
})
});
}
else {
this.router.navigate(['..', this.DASHBOARD_ROUTE_PATH, dashboardMO.id], {
relativeTo: getActivatedRoute(this.router),
...(isNewDashboard && {
state: { [NEW_DASHBOARD_ROUTER_STATE_PROP]: true }
})
});
}
}
/**
* Checks if user is able to edit dashboard according to his roles and dashboard ownership.
*
* @param mo - Dashboard managed object.
* @returns True if user is able to edit dashboard, false if he cannot.
*/
async canEditDashboard(mo) {
return await this.permissions.canEdit(this.INVENTORY_ROLES, mo);
}
/**
* Checks if user has permissions to copy dashboard according to his roles.
*
* @returns True if user has permissions to copy dashboard, false if he cannot.
*/
hasPermissionsToCopyDashboard() {
return this.permissions.hasAnyRole([
Permissions.ROLE_INVENTORY_ADMIN,
Permissions.ROLE_INVENTORY_CREATE,
Permissions.ROLE_MANAGED_OBJECT_ADMIN,
Permissions.ROLE_MANAGED_OBJECT_CREATE
]);
}
isNamed(dashboard) {
return some(keys(dashboard), prop => new RegExp(`^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}`).test(prop));
}
isReport(dashboard) {
return some(keys(dashboard), prop => new RegExp(`^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}${this.REPORT_PARTIAL_NAME}`).test(prop));
}
isDeviceType(dashboard) {
return some(keys(dashboard), prop => {
const matchingProp = new RegExp(`^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Type}${this.INDEX_SPLIT}`).test(prop);
if (!matchingProp) {
return false;
}
else {
// there might be matching key, but its value can be {} or null
return !!dashboard[prop];
}
});
}
isDeviceDashboard(dashboard) {
return some(keys(dashboard), prop => new RegExp(`^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Device}${this.INDEX_SPLIT}`).test(prop));
}
isGroupDashboard(dashboard) {
return some(keys(dashboard), prop => new RegExp(`^${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Group}${this.INDEX_SPLIT}`).test(prop));
}
getFilteredDashboardStyles(styleList) {
return styleList.filter(c => STYLING_CLASS_PREFIXES.some(classPrefix => c.startsWith(classPrefix)));
}
getStyling(styleList, styleName, defaultValue) {
const styling = styleList.find(style => style && new RegExp(`-${styleName}$`, 'i').test(style.class));
return styling ? styling.class : defaultValue;
}
mapWidgets(widgets) {
return keyBy(widgets.map(widget => {
widget.id = String(Math.random()).substr(2);
return widget;
}), 'id');
}
getDashboard$(dashboardIdOrName, dashboardType, mo) {
const cache = this.cache.get(dashboardIdOrName);
const dashboards = mo
? this.getContextDashboards(mo, dashboardType)
: this.getNamedDashboard(dashboardIdOrName);
const cacheRefresh = this.getContextDashboards$(dashboards).pipe(tap(dashboard => this.cacheDashboard(dashboard)), filter(dashboard => dashboard.id === dashboardIdOrName ||
has(dashboard, `${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}${dashboardIdOrName}`)));
return cache ? of(cache) : cacheRefresh;
}
async pasteDashboard(newContext) {
if (this.copyClipboard) {
try {
const dashboardToPaste = this.createContextDashboardCopy(this.copyClipboard.dashboard, newContext.contextData, this.copyClipboard.context.contextData);
const dashboard = await this.create(this.clean(dashboardToPaste), newContext);
// linking childAdditions for e.g. to grant access to the images uploaded by the image widget for users with only inventory roles.
const { data: childAdditions } = await this.inventory.childAdditionsList(this.copyClipboard.dashboardId, { pageSize: 2000 });
if (childAdditions.length) {
await this.inventory.childAdditionsBulkAdd(childAdditions, dashboard.id);
}
this.copyClipboard = undefined;
this.navigateToDashboard(dashboard);
}
catch {
this.alert.warning(gettext('Insufficient permissions for this action.'));
}
}
}
/**
* Creates fragment that associates dashboards with device/asset. It consists of three elements:
* - FRAGMENT_NAME - static string
* - dashboard type (e.g. 'group', 'device')
* - fragment value ( id of device/asset if it is not typed dashboard; deviceTypeValue property of dashboard if it is type dashboard)
* Example fragment for device dashboard: 'c8y_Dashboard!device!773200'
* Example fragment for group dashboard: 'c8y_Dashboard!group!84129208'
* Example fragment for typed device dashboard: 'c8y_Dashboard!type!c8y_lwm2m_connector_device'
*
* @param contextDashboardType Type of dashboard
* @param value Fragment value
* @returns Fragment for dashboard
*/
createFragmentKey(contextDashboardType, value) {
return `${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${contextDashboardType}${this.INDEX_SPLIT}${value}`;
}
/**
* Indicates if dashboard can be set to type dashboard.
* First, it checks if deviceTypeValue exists and if user has permission to set dashboard type.
* Then, case from sensor app is checked- dashboard created with sensor app has deviceType set to true but
* type fragment is missing- we do not support this combination.
* @param mo Dashboard managed object
* @param context {ContextData} Current context
* @returns True if dashboard can be set to type dashboard, false if it is forbidden.
*/
shouldAllowToSetDashboardType(mo, context) {
// disallow if dashboard managed object or context is missing or context is not device/asset/group
if (!mo ||
!context?.contextData ||
(context.context !== ViewContext.Device && context.context !== ViewContext.Group)) {
return 'disallow';
}
// if context is asset/group and type dashboard feature is hidden for assets/groups or asset/group has no typ, return disallow
const typeDashboardHiddenForAssets = this.optionsService.get(this.HIDE_TYPE_DASHBOARD_FOR_ASSETS, true);
if (context.context === ViewContext.Group &&
(typeDashboardHiddenForAssets || !context.contextData.type)) {
return 'disallow';
}
// if user has no permission to change dashboard, return disallow
if (!this.permissions.hasAnyRole(this.INVENTORY_ROLES)) {
return 'disallow';
}
// case from sensor app is checked- dashboard created with sensor app has deviceType set to true but
// type fragment is missing- we do not support this combination.
const typeFragment = this.createFragmentKey(ContextDashboardType.Type, context?.contextData?.type);
if (mo?.c8y_Dashboard &&
mo?.c8y_Dashboard.deviceType &&
context?.contextData?.type &&
!mo[typeFragment]) {
return 'disallow';
}
// if view context is Device and contextData of this device has no type yet but type dashboard can be set when type is filled,
// return allow_if_type_filled
if (!context?.contextData?.type &&
context.context === ViewContext.Device &&
this.permissions.hasAnyRole(this.INVENTORY_ROLES)) {
return 'allow_if_type_filled';
}
return 'allow';
}
createReport(reportCfg) {
const report = {};
Object.assign(report, reportCfg);
Object.assign(report, { c8y_Report: {} });
return this.inventory.create(report);
}
addReportNavigatorNode(report) {
const node = new NavigatorNode({
label: report.name,
path: `reports/${report.id}`,
icon: report.icon,
priority: report.priority
});
this.navigator.add(node);
}
getContextForGS(mo) {
if (this.groupService.isDevice(mo)) {
return PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.DEVICE;
}
else if (this.groupService.isAsset(mo)) {
return PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.ASSET;
}
else if (this.groupService.isGroup(mo)) {
return PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.GROUP;
}
else {
return null;
}
}
async getContextDashboards(mo, dashboardType) {
const filterCriteria = dashboardType.map(t => ({
// it's necessary to wrap fragment in quotes because dashboard type can contain spaces
__has: `'${this.createDashboardFragment(mo, t)}'`
}));
// the has query above does not work for device type dashboards where the type contains a dot
const typeFilterCriteria = dashboardType.includes(ContextDashboardType.Type) && mo.type
? {
__and: [
{ 'c8y_Dashboard.deviceType': { __eq: true } },
{ 'c8y_Dashboard.deviceTypeValue': { __eq: mo.type } }
]
}
: undefined;
const finalFilterCriteria = typeFilterCriteria
? [...filterCriteria, typeFilterCriteria]
: filterCriteria;
const query = this.queriesUtil.buildQuery({ __filter: { __or: finalFilterCriteria } });
const now = Date.now();
const cacheHasValidResponse = this.contextDashboardsCache &&
this.contextDashboardsCache.query === query &&
now - this.contextDashboardsCache.timestamp < this.CACHE_TIMEOUT;
if (cacheHasValidResponse) {
return this.contextDashboardsCache.result;
}
else {
this.contextDashboardsCache = null;
}
this.contextDashboardsCache = {
query,
result: this.inventory.list({ query, pageSize: this.DEFAULT_PAGESIZE }),
timestamp: now
};
return this.contextDashboardsCache.result;
}
/**
* Creates a tuple describing the dashboard type and its fragments. For assets like devices and groups, it's possible
* to have two fragments: one indicating this particular device/asset with its ID, and the second indicating
* the device/asset type (if the dashboard is meant to be applied to all assets of this type).
*
* @param dashboardMO - Dashboard managed object.
* @param context - Context data of asset.
* @param name - Name of the dashboard.
* @param isEdit - True if existing dashboard is updated, false when it's creation of new dashboard.
* @returns Tuple of dashboard type and object containing dashboard fragments.
*/
getDashboardFragments(dashboardMO, context, name, isEdit) {
let dashboardType;
const id = context?.contextData?.id || '';
const fragments = {};
if (name) {
// a named dashboard should not receive any other fragments
dashboardType = ContextDashboardType.Named;
const namedFragmentKey = this.createFragmentKey(ContextDashboardType.Named, name);
fragments[namedFragmentKey] = {};
}
else if (context?.context === ViewContext.Device || context?.context === ViewContext.Group) {
// get base type for device or group
const defaultType = context.context === ViewContext.Device
? ContextDashboardType.Device
: ContextDashboardType.Group;
dashboardType = dashboardMO.c8y_Dashboard.deviceType
? ContextDashboardType.Type
: defaultType;
// clear fragments from other asset if current asset is not origin of this dashboard
this.clearRedundantFragment(dashboardMO, defaultType, fragments);
// add base fragment for particular asset
const deviceOrGroupFragmentKey = this.createFragmentKey(defaultType, id);
fragments[deviceOrGroupFragmentKey] = {};
// add or clear type fragment
if (dashboardMO.c8y_Dashboard.deviceType || isEdit) {
const typeFragmentKey = this.createFragmentKey(ContextDashboardType.Type, dashboardMO.c8y_Dashboard.deviceTypeValue);
fragments[typeFragmentKey] = dashboardMO.c8y_Dashboard.deviceType ? {} : null;
}
}
return [dashboardType, fragments];
}
/**
* Clears fragments that originates from other managed object.
* E.g. typed dashboard is created for device A of type c8y_MQTTDevice and id 1, so it gets fragments object
* ```ts
* {
* c8y_Dashboard!device!1: {},
* c8y_Dashboard!type!c8y_MQTTDevice: {}
* }
*```
* then, on device B of type c8y_MQTTDevice and id 2, which also has access to this dashboard, deviceType is set to
* false, so dashboard is not typed dashboard anymore and now belongs to device B, therefore fragments should look like
* ```ts
* {
* c8y_Dashboard!device!1: null, // this value is cleared because dashboard is doesn't belong to device A anymore
* c8y_Dashboard!device!2: {}, // assign dashboard to device B
* c8y_Dashboard!type!c8y_MQTTDevice: null // this value is cleared in getDashboardFragments method as it's not typed dashboard anymore
* }
* ```
*
* @param dashboardMO - Dashboard managed object.
* @param type - Context dashboard type.
* @param fragments - Fragments object.
*/
clearRedundantFragment(dashboardMO, type, fragments) {
Object.keys(dashboardMO)
.filter(key => key.startsWith(this.createFragmentKey(type, '')))
.forEach(key => (fragments[key] = null));
}
adjustDashboardFor24Columns(dashboards) {
if (Array.isArray(dashboards)) {
return dashboards.map(dashboard => this.adjustDashboardFor24Columns(dashboard));
}
// if `columns` attribute exists, dashboard was already adjusted.
if (dashboards.c8y_Dashboard.columns) {
return dashboards;
}
dashboards.c8y_Dashboard.columns = 24;
if (!dashboards.c8y_Dashboard.children) {
return dashboards;
}
// Newly created NamedContextDashboards are still created with 12 columns for backwards compatibility.
// Default widgets might be already configured for 24 columns.
// If a widget is already configured for more than 12 columns, we should not adjust it.
const alreadyHasWidgetsConfiguredForMoreThan12Columns = Object.values(dashboards.c8y_Dashboard.children).some(widget => widget._x + widget._width > 12);
if (alreadyHasWidgetsConfiguredForMoreThan12Columns) {
return dashboards;
}
// we need to multiply both _width and _x attributes with 2 to migrate from 12 to 24 columns.
Object.values(dashboards.c8y_Dashboard.children).forEach(widget => {
if (widget._width) {
widget._width = widget._width * 2;
}
if (widget._x) {
widget._x = widget._x * 2;
}
});
return dashboards;
}
async serializeWidgetConfigs(dashboard) {
const children = cloneDeep(dashboard.c8y_Dashboard.children);
if (!children) {
return;
}
const configs = Object.values(children);
const details = configs.map(({ componentId, config }) => ({ componentId, config }));
const results = await this.dynamicComponent.serializeConfigs(details);
results.forEach((result, index) => {
Object.entries(result).forEach(([key, value]) => {
set(details[index].config, key, value);
});
});
dashboard.c8y_Dashboard.children = children;
}
createContextDashboardCopy(dash, newContext, oldContext) {
const children = reduce(dash.children, (_children, child) => {
const { id } = child;
const cfg = child.config;
const propertiesToCopy = {
device: device => this.replaceContextInObj(device, newContext, oldContext),
datapoints: dataPoints => this.replaceContextInDataPoints(dataPoints, newContext, oldContext),
dataPoints: dataPoints => this.replaceContextInDataPoints(dataPoints, newContext, oldContext),
datapointsGauge: dataPoints => this.replaceContextInDataPoints(dataPoints, newContext, oldContext),
datapointsLabels: dataPoints => this.replaceContextInDataPoints(dataPoints, newContext, oldContext)
};
if (cfg) {
this.copyProperties(cfg, propertiesToCopy);
if (cfg.options) {
this.copyProperties(cfg.options, propertiesToCopy);
}
}
_children[id] = cloneDeep(child);
return _children;
}, {});
dash.children = children;
const isTypeDashboard = dash.deviceType && !!dash.deviceTypeValue;
if (isTypeDashboard) {
dash.deviceTypeValue = newContext.type;
}
return dash;
}
copyProperties(obj, propertiesToCopy) {
forEach(propertiesToCopy, (copyFn, property) => {
if (obj[property]) {
obj[property] = copyFn(obj[property]);
}
});
}
replaceContextInDataPoints(dataPoints, newContext, oldContext) {
dataPoints.forEach(dp => {
dp.__target = this.replaceContextInObj(dp.__target, newContext, oldContext);
});
return dataPoints;
}
replaceContextInObj(obj, newContext, oldContext) {
if (obj && obj.id === oldContext.id) {
Object.assign(obj, pick(newContext, ['id', 'name']));
}
return obj;
}
getTabs$(mo, dashboardType, context) {
const dashboards = this.getContextDashboards(mo, dashboardType);
return this.getContextDashboards$(dashboards).pipe(mergeMap(dashboard => this.verifyDashboardAvailability$(dashboard)),
// Due to MTM-62321 named context dashboards included fragments for device and groups.
// therefore some device-info dashboards (from DM app) might occur in cockpit on device level.
// We need to filter those out..
filter(([dashboard]) => {
// list all identifiers of the dashboard (name, device, group, type)
const dashboardIdentifiers = Object.keys(dashboard)
.filter(key => key.startsWith(`${this.FRAGMENT_NAME}${this.INDEX_SPLIT}`))
.map(key => key.split(this.INDEX_SPLIT)[1]);
// if dashboard is not named, it's safe to include it
if (!dashboardIdentifiers.includes(ContextDashboardType.Named)) {
return true;
}
// if dashboard is named, but also has a context identifier, we skip it
if (dashboardIdentifiers.includes(ContextDashboardType.Device) ||
dashboardIdentifiers.includes(ContextDashboardType.Group)) {
return false;
}
// all others are good..
return true;
}), mergeMap(([dashboard]) => this.removeDashboardMoProperty(dashboard)), tap(dashboard => this.cacheDashboard(dashboard)), map(dashboard => this.createDashboardTab(dashboard, context)), toArray());
}
verifyDashboardAvailability$(dashboard) {
const globalRolesIds = dashboard?.c8y_Dashboard?.globalRolesIds;
const canEdit = from(this.permissions.canEdit(this.INVENTORY_ROLES, dashboard, { skipRequestCheck: true }));
const hasAnyGlobalRole = !globalRolesIds || globalRolesIds === ALL_GLOBAL_ROLES_SELECTED
? of(true)
: of(this.permissions.hasAnyGlobalRole(globalRolesIds));
return combineLatest([of(dashboard), canEdit, hasAnyGlobalRole]).pipe(filter(([, canEdit, hasAnyGlobalRole]) => canEdit || hasAnyGlobalRole));
}
getContextDashboards$(request) {
return from(request).pipe(mergeMap(response => this.adjustDashboardFor24Columns(response.data)));
}
/**
* Cleans already corrupted dashboards from dashboardMo property.
* Added to fix dashboards on the cloud instance (eu-latest).
* @deprecated This is going to be removed after 1007.7.0.
*/
async removeDashboardMoProperty(dashboard) {
const dashboardCopy = cloneDeep(dashboard);
const children = get(dashboardCopy, 'c8y_Dashboard.children');
let updateDashboard = false;
forEach(children, child => {
if (get(child, 'componentTransformConfigWithContext')) {
delete child.componentTransformConfigWithContext;
updateDashboard = true;
}
if (get(child, 'config.dashboardMo')) {
delete child.config.dashboardMo;
updateDashboard = true;
}
});
if (updateDashboard) {
await this.update(dashboardCopy);
}
return dashboardCopy;
}
cacheDashboard(dashboard) {
this.cache.set(dashboard.id, dashboard);
}
createDashboardTab(dashboard, context) {
const { c8y_Dashboard: _dashboard, id } = dashboard;
return {
icon: _dashboard.icon,
path: `${this.DASHBOARD_ROUTE_PATH}/${id}`,
label: _dashboard.name,
priority: _dashboard.priority,
hide: this.isReport(dashboard),
badge: _dashboard.deviceType && this.shouldAllowToSetDashboardType(dashboard, context)
? gettext('Dashboard template')
: null,
tooltipText: _dashboard.description || gettext('Dashboard template')
};
}
clean(dashboard) {
const jsonString = JSON.stringify(dashboard, (key, value) => {
if (key === '$$hashKey' || key === 'klasses') {
return undefined;
}
return value;
});
return JSON.parse(jsonString);
}
getNamedDashboard(name) {
return this.inventory.list({
fragmentType: `${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${ContextDashboardType.Named}${this.INDEX_SPLIT}${name}`,
pageSize: 1
});
}
createDashboardFragment(mo, type) {
let value;
if (mo.c8y_Report) {
value = `${this.REPORT_PARTIAL_NAME}${mo.id}`;
}
else {
value = type === ContextDashboardType.Type ? mo.type : mo.id;
}
return `${this.FRAGMENT_NAME}${this.INDEX_SPLIT}${type}${this.INDEX_SPLIT}${value}`;
}
shouldSetGlobal(dashboard, context) {
if ((!context && this.isNamed(dashboard) && !this.isReport(dashboard)) ||
this.isDeviceType(dashboard)) {
return {};
}
return null;
}
getDefaultDashboard(dashboardCfg) {
return {
c8y_Dashboard: {
...dashboardCfg,
name: dashboardCfg.name || 'Default Dashboard',
description: gettext('This is a default dashboard.')
},
[`c8y_Dashboard!name!${dashboardCfg.name}`]: {}
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardService, deps: [{ token: i1$1.InventoryService }, { token: i2.TabsService }, { token: i2.ModalService }, { token: i2$1.TranslateService }, { token: i1.Router }, { token: i2.NavigatorService }, { token: i2.Permissions }, { token: i2.AlertService }, { token: i2.DynamicComponentService }, { token: i2.GroupService }, { token: i2.OptionsService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: i1$1.InventoryService }, { type: i2.TabsService }, { type: i2.ModalService }, { type: i2$1.TranslateService }, { type: i1.Router }, { type: i2.NavigatorService }, { type: i2.Permissions }, { type: i2.AlertService }, { type: i2.DynamicComponentService }, { type: i2.GroupService }, { type: i2.OptionsService }] });
const DEFAULT_DUPLICATE_WITHOUT_TYPE = () => {
throw new Error('duplicateWithoutTypeFn Not implemented');
};
const DEFAULT_PREVIEW_CHANGED = () => {
throw new Error('previewChanged Not implemented');
};
const DEFAULT_REVERT_TO_DASHBOARD = () => {
throw new Error('revertToDashboard Not implemented');
};
class DashboardDetailService {
get generalSettingsForm() {
return this.dashboardDetailsForm.get('generalSettingsForm');
}
get appearanceSettingsForm() {
return this.dashboardDetailsForm.get('appearanceSettingsForm');
}
get widgetsForm() {
return this.dashboardDetailsForm.get('widgetsForm');
}
constructor(fb, translateService, tabsService) {
this.fb = fb;
this.translateService = translateService;
this.tabsService = tabsService;
this.saveButtonDisabled = false;
this.styling = {
themeClass: 'dashboard-theme-light',
headerClass: 'panel-title-regular'
};
this.duplicateWithoutTypeFn = DEFAULT_DUPLICATE_WITHOUT_TYPE;
this.previewChangedFn = DEFAULT_PREVIEW_CHANGED;
this.revertToDashboardFn = DEFAULT_REVERT_TO_DASHBOARD;
this.DEFAULT_DASHBOARD_MARGIN = 12;
this.DEFAULT_DASHBOARD_ICON = 'th';
this.DEFAULT_DASHBOARD_PRIORITY = 5000;
this.tabsService.items$
.pipe(map$1(tabs => tabs.filter(tab => tab.tabsOutlet === DASHBOARD_DETAILS_TABS_OUTLET_NAME)), takeUntilDestroyed())
.subscribe(tabs => (this.detailsTabs = tabs));
}
getTabs() {
let tabs = this.detailsTabs;
const shouldHideGeneralTab = !((!this.details?.isNamedDashboard && !this.details?.hideAvailability) ||
this.details?.isReport);
const shouldHideVersionHistoryTab = !this.details?.mo?.c8y_DashboardHistory;
if (shouldHideGeneralTab) {
tabs = tabs.filter(tab => tab.featureId !== DashboardDetailsTabId.GENERAL);
}
if (shouldHideVersionHistoryTab) {
tabs = tabs.filter(tab => tab.featureId !== DashboardDetailsTabId.VERSIONHISTORY);
}
return tabs;
}
/**
* Initializes dashboard details form.
* @param dashboard Context dashboard object
* @param isReport True if its report dashboard
* @param allowTypeDashboard Allow to make dashboard type dashboard
* @returns Dashboard details form
*/
initForm(dashboard, isReport, allowTypeDashboard) {
this.dashboardDetailsForm = this.createDashboardDetailsForm(dashboard, isReport, allowTypeDashboard);
}
reset() {
this.dashboardDetailsForm = null;
this.details = null;
this.duplicateWithoutTypeFn = DEFAULT_DUPLICATE_WITHOUT_TYPE;
this.previewChangedFn = DEFAULT_PREVIEW_CHANGED;
this.revertToDashboardFn = DEFAULT_REVERT_TO_DASHBOARD;
}
/**
* Creates dashboard details form divided into two groups- general settings and appearance settings.
* @param dashboard Context dashboard object
* @param isReport True if its report dashboard
* @param allowTypeDashboard Allow to make dashboard type dashboard
* @returns Dashboard details form
*/
createDashboardDetailsForm(dashboard, isReport, allowTypeDashboard) {
return this.fb.group({
generalSettingsForm: this.createGeneralSettingsForm(dashboard, isReport, allowTypeDashboard),
appearanceSettingsForm: this.createAppearanceSettingsForm(dashboard),
widgetsForm: this.createWidgetsForm(dashboard)
});
}
/**
* Creates form group for general settings of dashboard.
* @param dashboard Context dashboard object
* @param isReport True if its report dashboard
* @param allowTypeDashboard Allow to make dashboard type dashboard
* @returns Dashboard general settings form
*/
createGeneralSettingsForm(dashboard, isReport, allowTypeDashboard) {
const globalRolesIdsValue = !dashboard?.globalRolesIds
? null
: Array.isArray(dashboard.globalRolesIds)
? [...dashboard.globalRolesIds]
: dashboard.globalRolesIds;
return this.fb.group({
icon: [dashboard?.icon || this.DEFAULT_DASHBOARD_ICON, [Validators.minLength(1)]],
priority: [
dashboard?.priority || this.DEFAULT_DASHBOARD_PRIORITY,
[Validators.min(-10000), Validators.max(10000), Validators.required]
],
name: [
dashboard?.name ||
(isReport
? this.translateService.instant(gettext('Report'))
: this.translateService.instant(gettext('Dashboard'))),
[Validators.required, Validators.maxLength(512)]
],
description: [dashboard?.description || null, [Validators.maxLength(1_000)]],
globalRolesIds: this.fb.control(globalRolesIdsValue), // calling this.fb.control to cast type
c8y_IsNavigatorNode: [!!dashboard?.c8y_IsNavigatorNode],
...(allowTypeDashboard !== 'disallow' && {
deviceType: [
{
value: dashboard?.deviceType ?? false,
disabled: allowTypeDashboard === 'allow_if_type_filled'
}
]
})
});
}
/**
* Creates form group for appearance settings of dashboard.
* @param dashboard Context dashboard object
* @returns Dashboard appearance settings form
*/
createAppearanceSettingsForm(dashboard) {
return this.fb.group({
widgetMargin: [
dashboard?.widgetMargin || this.DEFAULT_DASHBOARD_MARGIN,
[Validators.min(0), Validators.max(50)]
],
translateWidgetTitle: [!!dashboard?.translateWidgetTitle],
classes: dashboard?.classes || { [this.styling.themeClass]: true },
widgetClasses: dashboard?.widgetClasses || { [this.styling.headerClass]: true }
});
}
createWidgetsForm(dashboard) {
return this.fb.group({
children: dashboard?.children || {}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailService, deps: [{ token: i5.FormBuilder }, { token: i2$1.TranslateService }, { token: i2.TabsService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i5.FormBuilder }, { type: i2$1.TranslateService }, { type: i2.TabsService }] });
class DashboardDetailComponent {
constructor(contextDashboardService, translateService, contextRoute, activatedRoute, tabsService, router, inventory, route, dashboardDetailService, appState, groupService, modal, gainsightService) {
this.contextDashboardService = contextDashboardService;
this.translateService = translateService;
this.contextRoute = contextRoute;
this.activatedRoute = activatedRoute;
this.tabsService = tabsService;
this.router = router;
this.inventory = inventory;
this.route = route;
this.dashboardDetailService = dashboardDetailService;
this.appState = appState;
this.groupService = groupService;
this.modal = modal;
this.gainsightService = gainsightService;
this.isCollapsed = true;
this.hideAvailability = false;
this.tabs = [];
this.dashboardSaved = new EventEmitter();
this.previewChanged = new EventEmitter();
this.destroy$ = new Subject();
this.TABS_OUTLET_NAME = DASHBOARD_DETAILS_TABS_OUTLET_NAME;
}
async ngOnInit() {
this.currentContext = this.contextRoute.getContextData(this.activatedRoute);
this.deviceTypeValue = this.context?.type;
this.displayDeviceTypeValue =
this.context &&
this.groupService.isGroup(this.context) &&
!this.groupService.isAsset(this.context)
? this.translateService.instant(gettext('Group'))
: this.deviceTypeValue;
this.updateAllowTypeDashboard();
this.initForm();
await this.clearNamedOutletPath(); // on init this component collapsed, but outlet may occur in URL when reloading page
}
ngOnChanges() {
this.updateAllowTypeDashboard();
this.refreshDashboardDetails();
}
ngAfterContentInit() {
if (!this.isEdit) {
this.initializeConfiguration();
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.dashboardDetailService.reset();
}
async save() {
const oldDashboard = cloneDeep(this.dashboard);
this.dashboard = {
...this.dashboard,
...this.dashboardDetailService.dashboardDetailsForm.get('generalSettingsForm').value,
...this.dashboardDetailService.dashboardDetailsForm.get('appearanceSettingsForm').value,
...this.dashboardDetailService.dashboardDetailsForm.get('widgetsForm').value,
c8y_IsNavigatorNode: this.dashboardDetailService.dashboardDetailsForm.get('generalSettingsForm').value.c8y_IsNavigatorNode
? {}
: this.currentDashboard
? null
: undefined
};
const dashboardSettingChanges = Object.keys(this.dashboard).filter(key => {
if (key === 'c8y_IsNavigatorNode') {
return !!oldDashboard[key] !== !!this.dashboard[key];
}
return !isEqual(oldDashboard[key], this.dashboard[key]);
});
this.dashboard.author = this.appState.currentUser.value.userName;
this.dashboard.deviceTypeValue ??= this.deviceTypeValue;
this.dashboard.historyDescription =
this.updateDashboardHistoryDescription(dashboardSettingChanges);
if (!this.isEdit && !this.isReport) {
await this.createNewDashboard();
}
else if (!this.isEdit && this.isReport) {
await this.createNewReport();
}
else {
await this.updateDashboard();
this.mo.c8y_DashboardHistory = [
this.dashboard,
...(this.mo?.c8y_DashboardHistory ? this.mo.c8y_DashboardHistory : [])
];
}
}
updateDashboardHistoryDescription(dashboardChanges) {
return {
changeType: this.isEdit ? 'update' : 'create',
dashboardSettingChanges: dashboardChanges
.filter(value => value !== 'deviceType')
.map(value => DASHBOARD_SETTINGS_CHANGES[value] || value),
deviceType: dashboardChanges.find(value => value === 'deviceType')
? this.dashboard.deviceType
: undefined
};
}
async cancel() {
if (!this.isEdit && !this.isReport) {
// dashboard creation in it's own tab- confirm and cleanup triggered in canDeactivate during routing
this.tabsService.items$.pipe(first()).subscribe((tabs) => {
const tab = tabs.find(t => t.featureId !== newDashboardTab.featureId);
this.router.navigate(typeof tab.path === 'string' ? [tab.path] : tab.path, {
replaceUrl: true
});
});
return;
}
else if (!this.isEdit && this.isReport) {
this.router.navigate(['reports']);
return;
}
else if (this.dashboardDetailService.dashboardDetailsForm.dirty &&
!(await this.confirmClosing())) {
return;
}
await this.clearNamedOutletPath();
this.cleanupAfterDetailsClose();
}
async canDeactivate(omitConfirm = false) {
let canDeactivate;
if (this.isCollapsed || !this.dashboardDetailService.dashboardDetailsForm.dirty) {
canDeactivate = true;
}
else {
if (omitConfirm) {
canDeactivate = false;
}
else {
canDeactivate = await this.confirmClosing();
}
}
if (canDeactivate) {
this.cleanupAfterDetailsClose();
}
return canDeactivate;
}
show(isEdit) {
this.isEdit = isEdit;
if (this.isEdit) {
this.edit();
}
this.initForm();
this.isCollapsed = false;
this.refreshDashboardDetails();
}
async revertDashboard(dashboard) {
this.dashboard = dashboard;
this.dashboard.c8y_IsNavigatorNode = this.dashboard.c8y_IsNavigatorNode
? {}
: this.currentDashboard
? null
: undefined;
await this.updateDashboard();
}
async duplicateWithoutType() {
const newDashboardName = this.translateService.instant(gettext('{{ dashboardName }} (copy)'), {
dashboardName: this.dashboardDetailService.dashboardDetailsForm.get('generalSettingsForm').value.name
});
this.dashboardDetailService.dashboardDetailsForm.get('generalSettingsForm').patchValue({
...this.dashboardDetailService.dashboardDetailsForm.get('generalSettingsForm').value,
deviceType: false,
name: newDashboardName
});
// setting edition to false so new dashboard can be created as copy of current one
this.isEdit = false;
await this.save();
}
updateAllowTypeDashboard() {
this.allowTypeDashboard = this.contextDashboardService.shouldAllowToSetDashboardType(this.mo, this.currentContext);
}
initForm() {
this.dashboardDetailService.initForm(this.dashboard, this.isReport, this.allowTypeDashboard);
this.dashboardDetailsForm = this.dashboardDetailService.dashboardDetailsForm;
}
async confirmClosing() {
try {
await this.modal.confirm(gettext('Close dashboard details'), gettext('Are you sure you want to close dashboard details? All unsaved changes will be lost.'), Status.WARNING, {
ok: gettext('Close'),
cancel: gettext('Cancel')
});
return true;
}
catch (e) {
return false;
}
}
async createNewDashboard() {
const mo = await this.contextDashboardService.create(this.dashboard, this.currentContext, this.isNamedDashboard ? this.dashboard.name : undefined);
await this.onCreateGSEvent(mo);
await this.clearNamedOutletPath();
this.cleanupAfterDetailsClose();
await this.contextDashboardService.navigateToDashboard(mo, true);
}
async onCreateGSEvent(mo) {
const parentName = await this.convertStringToHash(this.context?.name);
const dashboardName = await this.convertStringToHash(this.dashboard?.name);
this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE.DASHBOARD.EVENTS.DASHBOARDS, {
component: PRODUCT_EXPERIENCE.DASHBOARD.COMPONENTS.ADD_DASHBOARD,
action: PRODUCT_EXPERIENCE.DASHBOARD.ACTIONS.CREATE,
name: dashboardName,
id: mo.id,
nameId: `${dashboardName}_${this.mo.id}`,
isDashboardTemplate: mo.c8y_Dashboard.deviceType,
parentAssetId: this.context?.id ? this.context.id : 'noContext',
parentAssetName: this.context?.id ? parentName : 'noContext',
parentAssetType: this.context?.id ? this.context.type : 'noContext',
parentAssetNameId: this.context?.id ? `${parentName}_${this.context.id}` : 'noContext',
parentAssetNameDashboardName: this.context?.id
? `${parentName}_${dashboardName}`
: 'noContext',
parentAssetIdDashboardId: this.context?.id ? `${this.context.id}_${this.mo.id}` : 'noContext',
parentAssetNameDashboardId: this.context?.id ? `${parentName}_${this.mo.id}` : 'noContext',
parentAssetNameIdDashboardNameId: this.context?.id
? `${parentName}_${this.context.id}_${dashboardName}_${this.mo.id}`
: 'noContext',
dashboardType: mo.c8y_Dashboard.deviceType ? mo.c8y_Dashboard.deviceTypeValue : null,
context: this.contextDashboardService.getContextForGS(this.currentContext?.contextData)
});
}
async convertStringToHash(str) {
if (!str) {
return null;
}
return (await this.gainsightService.shouldSendPiiData())
? str
: await this.gainsightService.hashGroupName(str);
}
async createNewReport() {
try {
const { name, icon, c8y_IsNavigatorNode, priority, description } = this.dashboard;
const report = (await this.contextDashboardService.createReport({
name,
icon,
c8y_IsNavigatorNode,
priority,
description
})).data;
await this.contextDashboardService.create(this.dashboard, undefined, `${this.contextDashboardService.REPORT_PARTIAL_NAME}${report.id}`);
this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE.DASHBOARD.EVENTS.REPORTS, {
component: PRODUCT_EXPERIENCE.DASHBOARD.COMPONENTS.ADD_REPORT,
action: PRODUCT_EXPERIENCE.DASHBOARD.ACTIONS.CREATE,
name: report.name,
id: report.id,
context: PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.REPORT
});
if (report.c8y_IsNavigatorNode) {
this.contextDashboardService.addReportNavigatorNode(report);
}
await this.clearNamedOutletPath();
this.cleanupAfterDetailsClose();
await this.router.navigate(['reports', report.id]);
}
catch (ex) {
// intended empty
}
}
async updateDashboard() {
try {
const dashboardMO = cloneDeep(this.mo);
const cfg = this.dashboard;
if (this.isReport) {
const { name, icon, c8y_IsNavigatorNode, priority, description } = cfg;
dashboardMO.c8y_Dashboard = cfg;
await this.updateReport({
id: this.context.id,
name,
icon,
c8y_IsNavigatorNode,
priority,
description
});
}
else {
dashboardMO.c8y_Dashboard = cfg;
}
const updatedDashboardMo = await this.contextDashboardService.update(dashboardMO, this.currentContext);
await this.clearNamedOutletPath();
this.cleanupAfterDetailsClose();
this.dashboardSaved.emit(updatedDashboardMo);
}
catch (ex) {
// intended empty
}
}
edit() {
if (this.isReport) {
const { name, icon, priority, c8y_IsNavigatorNode, description } = this.context;
Object.assign(this.dashboard, { name, icon, priority, c8y_IsNavigatorNode, description });
}
this.initializeConfiguration();
}
initializeConfiguration() {
const defaultDashboardCfg = {
name: this.translateService.instant(this.isReport ? gettext('Report') : gettext('Dashboard')),
priority: this.isReport
? REPORT_DEFAULT_NAVIGATION_NODE_PRIORITY
: this.dashboardDetailService.DEFAULT_DASHBOARD_PRIORITY,
icon: this.dashboardDetailService.DEFAULT_DASHBOARD_ICON,
deviceTypeValue: this.deviceType,
columns: 24
};
if (this.dashboard) {
this.currentDashboard = clone(this.dashboard);
}
else {
this.dashboard = defaultDashboardCfg;
this.dashboardDetailService.dashboardDetailsForm.markAsDirty();
}
}
async updateReport(mo) {
mo.c8y_IsNavigatorNode = mo.c8y_IsNavigatorNode ? {} : null;
const res = await this.inventory.update(mo);
this.context = res.data;
if (this.route.parent) {
this.route.parent.snapshot.data.contextData = this.context;
}
this.contextDashboardService.updateNavigatorItem(res.data);
}
cleanupAfterDetailsClose() {
this.isCollapsed = true;
if (!this.isEdit) {
const dashboardContext = this.route.snapshot.data.dashboard;
// hide New dashboard tab
const tmpNewDashboardTab = [...this.tabsService.state].find(t => t.featureId === newDashboardTab.featureId);
// don't hide the new dashboard tab when clicking on the add dashboard button from another dashboard.
if (!tmpNewDashboardTab || dashboardContext?.c8y_Dashboard) {
return;
}
// hide tab and make it appear as last one to prevent navigating to it
tmpNewDashboardTab.hide = true;
tmpNewDashboardTab.priority = -Infinity;
this.tabsService.refresh();
}
this.previewChanged.emit(this.dashboard);
}
async clearNamedOutletPath() {
if (this.activatedRoute.snapshot?.children?.[0]?.routeConfig.outlet === DASHBOARD_DETAILS_OUTLET) {
await this.router.navigate([
{
outlets: {
[DASHBOARD_DETAILS_OUTLET]: null
}
}
], { queryParamsHandling: 'preserve', relativeTo: this.activatedRoute });
}
}
refreshDashboardDetails() {
this.dashboardDetailService.details = {
isReport: this.isReport,
isNamedDashboard: this.isNamedDashboard,
hideAvailability: this.hideAvailability,
dashboard: this.dashboard,
deviceTypeValue: this.deviceType,
displayDeviceTypeValue: this.displayDeviceTypeValue,
mo: this.mo,
allowTypeDashboard: this.allowTypeDashboard,
isDevice: !!this.context?.c8y_IsDevice,
context: this.context
};
this.dashboardDetailService.duplicateWithoutTypeFn = () => this.duplicateWithoutType();
this.dashboardDetailService.previewChangedFn = dashboardClasses => this.previewChanged.emit(dashboardClasses);
this.dashboardDetailService.revertToDashboardFn = dashboard => this.revertDashboard(dashboard);
this.tabs = this.dashboardDetailService.getTabs();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailComponent, deps: [{ token: ContextDashboardService }, { token: i2$1.TranslateService }, { token: i2.ContextRouteService }, { token: i1.ActivatedRoute }, { token: i2.TabsService }, { token: i1.Router }, { token: i1$1.InventoryService }, { token: i1.ActivatedRoute }, { token: DashboardDetailService }, { token: i2.AppStateService }, { token: i2.GroupService }, { token: i2.ModalService }, { token: i2.GainsightService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DashboardDetailComponent, isStandalone: true, selector: "c8y-dashboard-detail", inputs: { isReport: "isReport", deviceType: "deviceType", context: "context", mo: "mo", dashboard: "dashboard", isNamedDashboard: "isNamedDashboard", hideAvailability: "hideAvailability" }, outputs: { dashboardSaved: "dashboardSaved", previewChanged: "previewChanged" }, providers: [DashboardDetailService], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"collapse c8y-top-drawer\"\n [collapse]=\"isCollapsed\"\n [isAnimated]=\"true\"\n>\n <div class=\"p-t-16 p-b-16 p-l-24 p-r-24 separator-bottom\">\n <span class=\"h4\">{{ 'Dashboard settings' | translate }}</span>\n </div>\n\n <div\n class=\"d-flex\"\n *ngIf=\"!isCollapsed\"\n >\n <c8y-tabs-outlet\n class=\"c8y-top-drawer--tabs\"\n [tabs]=\"tabs\"\n [outletName]=\"TABS_OUTLET_NAME\"\n [orientation]=\"'vertical'\"\n [openFirstTab]=\"true\"\n ></c8y-tabs-outlet>\n\n <div\n class=\"inner-scroll c8y-top-drawer__tabs-container flex-grow p-l-24 p-r-lg-32 p-r-xs-24 p-r-sm-24\"\n >\n <router-outlet name=\"dashboard-details\"></router-outlet>\n </div>\n </div>\n\n <div class=\"c8y-top-drawer--footer\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n data-cy=\"dashboard-detail--cancel-dashboard\"\n (click)=\"cancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\n data-cy=\"dashboard-detail--save-dashboard\"\n (click)=\"save()\"\n [disabled]=\"((dashboardDetailsForm?.invalid || dashboardDetailsForm?.pristine) && this.isEdit) || dashboardDetailService.saveButtonDisabled\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TabsOutletComponent, selector: "c8y-tabs-outlet,c8y-ui-tabs", inputs: ["tabs", "orientation", "navigatorOpen", "outletName", "context", "openFirstTab", "hasHeader"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
__decorate([
memoize(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], DashboardDetailComponent.prototype, "convertStringToHash", null);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-dashboard-detail', providers: [DashboardDetailService], imports: [CollapseDirective, NgIf, TabsOutletComponent, RouterOutlet, C8yTranslatePipe], template: "<div\n class=\"collapse c8y-top-drawer\"\n [collapse]=\"isCollapsed\"\n [isAnimated]=\"true\"\n>\n <div class=\"p-t-16 p-b-16 p-l-24 p-r-24 separator-bottom\">\n <span class=\"h4\">{{ 'Dashboard settings' | translate }}</span>\n </div>\n\n <div\n class=\"d-flex\"\n *ngIf=\"!isCollapsed\"\n >\n <c8y-tabs-outlet\n class=\"c8y-top-drawer--tabs\"\n [tabs]=\"tabs\"\n [outletName]=\"TABS_OUTLET_NAME\"\n [orientation]=\"'vertical'\"\n [openFirstTab]=\"true\"\n ></c8y-tabs-outlet>\n\n <div\n class=\"inner-scroll c8y-top-drawer__tabs-container flex-grow p-l-24 p-r-lg-32 p-r-xs-24 p-r-sm-24\"\n >\n <router-outlet name=\"dashboard-details\"></router-outlet>\n </div>\n </div>\n\n <div class=\"c8y-top-drawer--footer\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n data-cy=\"dashboard-detail--cancel-dashboard\"\n (click)=\"cancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"button\"\n data-cy=\"dashboard-detail--save-dashboard\"\n (click)=\"save()\"\n [disabled]=\"((dashboardDetailsForm?.invalid || dashboardDetailsForm?.pristine) && this.isEdit) || dashboardDetailService.saveButtonDisabled\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n</div>\n" }]
}], ctorParameters: () => [{ type: ContextDashboardService }, { type: i2$1.TranslateService }, { type: i2.ContextRouteService }, { type: i1.ActivatedRoute }, { type: i2.TabsService }, { type: i1.Router }, { type: i1$1.InventoryService }, { type: i1.ActivatedRoute }, { type: DashboardDetailService }, { type: i2.AppStateService }, { type: i2.GroupService }, { type: i2.ModalService }, { type: i2.GainsightService }], propDecorators: { isReport: [{
type: Input
}], deviceType: [{
type: Input
}], context: [{
type: Input
}], mo: [{
type: Input
}], dashboard: [{
type: Input
}], isNamedDashboard: [{
type: Input
}], hideAvailability: [{
type: Input
}], dashboardSaved: [{
type: Output
}], previewChanged: [{
type: Output
}], convertStringToHash: [] } });
/**
* Holds current state of dashboard children during edit mode.
*/
class DashboardOriginatorService {
/**
* Current dashboard children state name for indicating what change will be undone.
*/
get currentStateName() {
return this.state?.name;
}
/**
* Initializes dashboard children state on edit mode start.
* @param state Dashboard children initial state.
*/
initState(state) {
this.state = state;
}
/**
* Updates dashboard children state when it is changed or restored with undo/redo.
* @param state Dashboard children updated state.
*/
setState(state) {
this.state = state;
}
/**
* Returns copy of current state.
*/
getState() {
return cloneDeep(this.state);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardOriginatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardOriginatorService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardOriginatorService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Caretaker is responsible for undo and redo functionalities.
*/
class DashboardCaretakerService {
/**
* Current undo stack length for defining undo button disable state.
*/
get undoStackLength() {
return this.undoStack.length;
}
/**
* Redo stack latest item for defining redo button disable state and its' title.
*/
get redoStackLastItem() {
return this.redoStack[this.redoStack.length - 1];
}
constructor(originator) {
this.originator = originator;
/**
* List of dashboard children changes.
* @private
*/
this.undoStack = [];
/**
* List of dashboard children changes that were undone and can be redone back.
* @private
*/
this.redoStack = [];
}
/**
* Adds current dashboard children state to undo stack and clears redo stack.
*/
backup() {
this.undoStack.push(this.originator.getState());
this.redoStack = [];
}
/**
* Reverts last dashboard children change and puts it in redo stack.
* @returns Current state of dashboard children after reverting last change.
*/
undo() {
if (!this.undoStack.length) {
return;
}
const memento = this.undoStack.pop();
this.redoStack.push(this.originator.getState());
this.originator.setState(memento);
return memento;
}
/**
* Applies previously undone change.
* @returns Current state of dashboard children after applying previously undone change.
*/
redo() {
if (!this.redoStack.length) {
return;
}
this.undoStack.push(this.originator.getState());
const memento = this.redoStack.pop();
this.originator.setState(memento);
return memento;
}
/**
* Clears both undo and redo stack, sets current state as initial state and returns initial state.
* @returns Initial state of dashboard children.
*/
reset() {
const initialItem = this.undoStack[0] || this.originator.getState();
this.undoStack = [];
this.redoStack = [];
this.originator.initState(initialItem);
return initialItem;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardCaretakerService, deps: [{ token: DashboardOriginatorService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardCaretakerService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardCaretakerService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: DashboardOriginatorService }] });
/**
* Facade for implementation of Memento design pattern (caretaker, originator). Responsible for undo/redo feature
* during edit mode of dashboard.
*/
class DashboardEditModeService {
get undoButtonDisabled() {
return this.caretaker.undoStackLength === 0;
}
/**
* Current dashboard change name for undo button title.
*/
get changeToUndoName() {
return this.originator.currentStateName;
}
/**
* Redo stack latest item for defining redo button disable state and its title.
*/
get redoStackLastItem() {
return this.caretaker.redoStackLastItem;
}
constructor(originator, caretaker) {
this.originator = originator;
this.caretaker = caretaker;
}
/**
* Initializes dashboard children state on edit mode start.
* @param state Dashboard children initial state.
*/
init(state) {
this.originator.initState(state);
}
/**
* Saves current state for further undo actions and sets new dashboard children state.
* @param state Dashboard children updated state.
*/
newState(state) {
this.caretaker.backup();
this.originator.setState(state);
}
/**
* Returns copy of current state.
*/
getCurrentState() {
return this.originator.getState();
}
/**
* Reverts last dashboard children change.
* @returns Current state of dashboard children after reverting last change.
*/
undo() {
return this.caretaker.undo();
}
/**
* Applies previously undone change.
* @returns Current state of dashboard children after applying previously undone change.
*/
redo() {
return this.caretaker.redo();
}
/**
* Resets dashboard children state to initial one and returns initial state.
* @returns Initial state of dashboard children.
*/
reset() {
return this.caretaker.reset();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardEditModeService, deps: [{ token: DashboardOriginatorService }, { token: DashboardCaretakerService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardEditModeService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardEditModeService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: DashboardOriginatorService }, { type: DashboardCaretakerService }] });
/**
* A hook to add dynamic configurations to any widget configuration.
* @deprecated Consider using the `hookWidgetConfig` function instead.
*/
const HOOK_WIDGET_CONFIG = new InjectionToken('HOOK_WIDGET_CONFIG');
/**
* A hook to add dynamic components to the UI (e.g. widgets).
*
* You can either provide a single `DynamicComponentDefinition` as parameter:
* ```typescript
* hookWidgetConfig(...)
* ```
*
* Or an array to directly register multiple:
* ```typescript
* hookWidgetConfig([...])
* ```
*
* Or you provide an Service that implements `ExtensionFactory<DynamicComponentDefinition>`
* ```typescript
* export class MyDynamicComponentDefinitionFactory implements ExtensionFactory<DynamicComponentDefinition> {...}
* ...
* hookWidgetConfig(MyDynamicComponentDefinitionFactory)
* ```
* A typed alternative to `HOOK_WIDGET_CONFIG`.
* @param components The `DynamicComponentDefinition`'s or `ExtensionFactory` to be provided.
* @returns An `Provider` to be provided in your module.
*/
function hookWidgetConfig(components, options) {
return hookGeneric(components, HOOK_WIDGET_CONFIG, options);
}
class WidgetConfigSectionService extends ExtensionPointForPlugins {
constructor(rootInjector, router, plugins) {
super(rootInjector, plugins);
this.router = router;
this.plugins = plugins;
this.DEFAULT_WAIT_TIME = 100;
this.items$ = this.setupItemsObservable();
}
get state() {
return this.state$.value;
}
add(component) {
this.state.add(component);
this.emitNewState();
}
remove(component) {
this.state.delete(component);
this.emitNewState();
}
setupItemsObservable() {
const remotesLoaded$ = this.plugins.allPluginsLoaded$.pipe(filter(loaded => !!loaded), take(1));
return fromTriggerOnce(this.router, this.refresh$, [
getInjectedHooks(HOOK_WIDGET_CONFIG, this.injectors),
() => this.factories,
stateToFactory(this.state$)
]).pipe(distinctUntilChanged(), takeUntil(combineLatest([timer(this.DEFAULT_WAIT_TIME), remotesLoaded$])), shareReplay(1));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigSectionService, deps: [{ token: i0.Injector }, { token: i1.Router }, { token: i2.PluginsResolveService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigSectionService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigSectionService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i0.Injector }, { type: i1.Router }, { type: i2.PluginsResolveService }] });
class WidgetConfigSectionComponent {
setFeedbackTemplate(template) {
this.feedbackTemplate = template;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetConfigSectionComponent, isStandalone: true, selector: "c8y-widget-config-section", inputs: { section: "section" }, ngImport: i0, template: "<c8y-li\n class=\"c8y-list__item--no-expand c8y-list__item--sticky-top c8y-list__item--dense full-w-collapse\"\n [collapsed]=\"!section.expanded\"\n #li\n>\n <div class=\"d-flex p-r-16 fit-w\">\n <button\n class=\"btn-clean flex-grow flex-no-shrink\"\n type=\"button\"\n [attr.data-cy]=\"section.label\"\n (click)=\"li.collapsed = !li.collapsed\"\n >\n <i\n class=\"p-r-4\"\n [c8yIcon]=\"li.collapsed ? 'chevron-right' : 'chevron-down'\"\n ></i>\n <span\n class=\"text-medium\"\n translate\n >\n {{ section.label }}\n </span>\n </button>\n <ng-container *ngTemplateOutlet=\"feedbackTemplate\"></ng-container>\n </div>\n <c8y-list-item-collapse>\n <ng-container\n *c8yComponentOutlet=\"\n section.component;\n initialState: section.initialState;\n environmentInjector: section.injector\n \"\n ></ng-container>\n </c8y-list-item-collapse>\n</c8y-li>\n", dependencies: [{ kind: "ngmodule", type: ListGroupModule }, { kind: "component", type: i2.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i2.ListItemCollapseComponent, selector: "c8y-list-item-collapse, c8y-li-collapse", inputs: ["collapseWay"] }, { kind: "directive", type: C8yComponentOutlet, selector: "[c8yComponentOutlet]", inputs: ["c8yComponentOutlet", "c8yComponentOutletInjector", "c8yComponentOutletEnvironmentInjector", "c8yComponentOutletProviders", "c8yComponentOutletInitialState"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: CommonModule }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigSectionComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-widget-config-section', standalone: true, imports: [ListGroupModule, C8yComponentOutlet, IconDirective, NgTemplateOutlet, CommonModule], template: "<c8y-li\n class=\"c8y-list__item--no-expand c8y-list__item--sticky-top c8y-list__item--dense full-w-collapse\"\n [collapsed]=\"!section.expanded\"\n #li\n>\n <div class=\"d-flex p-r-16 fit-w\">\n <button\n class=\"btn-clean flex-grow flex-no-shrink\"\n type=\"button\"\n [attr.data-cy]=\"section.label\"\n (click)=\"li.collapsed = !li.collapsed\"\n >\n <i\n class=\"p-r-4\"\n [c8yIcon]=\"li.collapsed ? 'chevron-right' : 'chevron-down'\"\n ></i>\n <span\n class=\"text-medium\"\n translate\n >\n {{ section.label }}\n </span>\n </button>\n <ng-container *ngTemplateOutlet=\"feedbackTemplate\"></ng-container>\n </div>\n <c8y-list-item-collapse>\n <ng-container\n *c8yComponentOutlet=\"\n section.component;\n initialState: section.initialState;\n environmentInjector: section.injector\n \"\n ></ng-container>\n </c8y-list-item-collapse>\n</c8y-li>\n" }]
}], propDecorators: { section: [{
type: Input
}] } });
class WidgetConfigFeedbackComponent {
constructor() {
this.widgetSection = inject(WidgetConfigSectionComponent);
}
ngAfterViewInit() {
this.widgetSection.setFeedbackTemplate(this.template);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigFeedbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetConfigFeedbackComponent, isStandalone: true, selector: "c8y-widget-config-feedback", viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0, template: "<ng-template #tpl>\n <ng-content></ng-content>\n</ng-template>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigFeedbackComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-widget-config-feedback', standalone: true, imports: [WidgetConfigSectionComponent], template: "<ng-template #tpl>\n <ng-content></ng-content>\n</ng-template>\n" }]
}], propDecorators: { template: [{
type: ViewChild,
args: [TemplateRef]
}] } });
class WidgetAssetSelectorComponent {
constructor() {
this.widgetConfigService = inject(WidgetConfigService);
this.sectionComponent = inject(WidgetConfigSectionComponent);
this.inventoryService = inject(InventoryService);
this.alertService = inject(AlertService);
/**
* If set to true, a device must be selected. Default is true.
*/
this.isRequired = true;
}
/**
* @ignore
*/
async ngOnInit() {
if (this.widgetConfigService.currentConfig.device) {
try {
if (this.widgetConfigService.currentConfig.device.self) {
this.selectedDevice = this.widgetConfigService.currentConfig.device;
}
else {
this.selectedDevice = (await this.inventoryService.detail(this.widgetConfigService.currentConfig.device.id)).data;
}
}
catch (ex) {
// intended empty catch block to handle cases where the device might not be found
// error messages however will be displayed in the widget config feedback component
}
}
else if (this.isRequired && !this.hasContext(this.widgetConfigService.currentConfig)) {
this.sectionComponent.section.expanded = true;
}
else if (this.hasContext(this.widgetConfigService.currentConfig)) {
this.selectedDevice = this.widgetConfigService.currentConfig.settings?.context;
}
this.assetSelectorConfig = {
view: 'miller',
groupsSelectable: this.groupsSelectable || this.widgetConfigService.currentConfig.settings?.groupsSelectable,
showChildDevices: true,
columnHeaders: true,
showUnassignedDevices: this.showUnassigned ?? true,
search: !this.widgetConfigService.currentConfig.settings?.context?.additionParents,
showFilter: true,
showSelected: false,
singleColumn: true,
label: '',
modelMode: 'full',
preventInitialSelect: false,
...this.assetSelectorConfig
};
}
/**
* Returns true if a context is set.
* @param value The current configuration of the widget.
* @returns true if a context is set, false otherwise.
*/
hasContext(value) {
return value?.settings?.context && Object.keys(value.settings.context).length > 0;
}
/**
* Update the configuration of the widget if the selection was changed.
* @param $event The change event of the selection.
*/
selectionChanged($event) {
if ($event.change.isSelected) {
this.updateConfig($event.change.item);
}
}
updateConfig(device) {
this.widgetConfigService.updateConfig({ device });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetAssetSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetAssetSelectorComponent, isStandalone: true, selector: "c8y-widget-asset-selector", inputs: { isRequired: "isRequired", showUnassigned: "showUnassigned", groupsSelectable: "groupsSelectable", assetSelectorConfig: "assetSelectorConfig", selectedDevice: "selectedDevice" }, host: { classAttribute: "bg-level-1" }, ngImport: i0, template: "<div class=\"card borderless\">\n <div\n class=\"card-block p-0 bg-inherit\"\n style=\"height: 315px\"\n >\n <!-- \n ngIf is needed to avoid a race condition as \n asset-selector seems not to support dynamic config changes\n -->\n <c8y-asset-selector-miller\n class=\"d-block bg-inherit p-relative\"\n *ngIf=\"assetSelectorConfig\"\n (onSelected)=\"selectionChanged($event)\"\n [config]=\"assetSelectorConfig\"\n [asset]=\"(widgetConfigService.currentConfig$ | async).settings?.context\"\n [(ngModel)]=\"selectedDevice\"\n [required]=\"isRequired\"\n name=\"configAsset\"\n ></c8y-asset-selector-miller>\n </div>\n\n <c8y-widget-config-feedback>\n <span\n class=\"tag chip text-12 m-4\"\n [ngClass]=\"{\n 'tag--info': selectedDevice || !isRequired,\n 'tag--danger': !selectedDevice && isRequired\n }\"\n >\n <button\n class=\"btn-clean text-12 m-r-4\"\n title=\"{{ 'Deselect' | translate }}\"\n type=\"button\"\n *ngIf=\"selectedDevice\"\n (click)=\"selectedDevice = null; updateConfig(null)\"\n >\n <i c8yIcon=\"times\"></i>\n </button>\n <span\n class=\"text-truncate\"\n title=\"{{ selectedDevice.name || 'Device ' + selectedDevice.id }}\"\n *ngIf=\"selectedDevice\"\n >\n {{ selectedDevice.name || 'Device ' + selectedDevice.id }}\n </span>\n <span\n *ngIf=\"!selectedDevice\"\n translate\n >\n No asset selected\n </span>\n </span>\n </c8y-widget-config-feedback>\n</div>\n", dependencies: [{ kind: "ngmodule", type: AssetSelectorModule }, { kind: "component", type: i1$2.MillerViewComponent, selector: "c8y-asset-selector-miller", inputs: ["config", "asset", "selectedDevice", "rootNode", "container"], outputs: ["onSelected", "onClearSelected"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "component", type: WidgetConfigFeedbackComponent, selector: "c8y-widget-config-feedback" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetAssetSelectorComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-widget-asset-selector', host: { class: 'bg-level-1' }, standalone: true, imports: [
AssetSelectorModule,
AsyncPipe,
WidgetConfigFeedbackComponent,
NgIf,
NgClass,
C8yTranslatePipe,
IconDirective,
FormsModule
], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], template: "<div class=\"card borderless\">\n <div\n class=\"card-block p-0 bg-inherit\"\n style=\"height: 315px\"\n >\n <!-- \n ngIf is needed to avoid a race condition as \n asset-selector seems not to support dynamic config changes\n -->\n <c8y-asset-selector-miller\n class=\"d-block bg-inherit p-relative\"\n *ngIf=\"assetSelectorConfig\"\n (onSelected)=\"selectionChanged($event)\"\n [config]=\"assetSelectorConfig\"\n [asset]=\"(widgetConfigService.currentConfig$ | async).settings?.context\"\n [(ngModel)]=\"selectedDevice\"\n [required]=\"isRequired\"\n name=\"configAsset\"\n ></c8y-asset-selector-miller>\n </div>\n\n <c8y-widget-config-feedback>\n <span\n class=\"tag chip text-12 m-4\"\n [ngClass]=\"{\n 'tag--info': selectedDevice || !isRequired,\n 'tag--danger': !selectedDevice && isRequired\n }\"\n >\n <button\n class=\"btn-clean text-12 m-r-4\"\n title=\"{{ 'Deselect' | translate }}\"\n type=\"button\"\n *ngIf=\"selectedDevice\"\n (click)=\"selectedDevice = null; updateConfig(null)\"\n >\n <i c8yIcon=\"times\"></i>\n </button>\n <span\n class=\"text-truncate\"\n title=\"{{ selectedDevice.name || 'Device ' + selectedDevice.id }}\"\n *ngIf=\"selectedDevice\"\n >\n {{ selectedDevice.name || 'Device ' + selectedDevice.id }}\n </span>\n <span\n *ngIf=\"!selectedDevice\"\n translate\n >\n No asset selected\n </span>\n </span>\n </c8y-widget-config-feedback>\n</div>\n" }]
}], propDecorators: { isRequired: [{
type: Input
}], showUnassigned: [{
type: Input
}], groupsSelectable: [{
type: Input
}], assetSelectorConfig: [{
type: Input
}], selectedDevice: [{
type: Input
}] } });
class WidgetConfigGeneralComponent {
constructor() {
this.widgetConfigService = inject(WidgetConfigService);
}
ngAfterViewInit() {
if (this.dynamicComponent) {
this.widgetConfigService.selectedComponent$.next(this.dynamicComponent);
this.widgetConfigService.addOnBeforeSave(() => this.dynamicComponent.callLifeCycleHooks());
}
}
ngOnDestroy() {
this.widgetConfigService.selectedComponent$.next(null);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigGeneralComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetConfigGeneralComponent, isStandalone: true, selector: "c8y-widget-config-general", inputs: { definition: "definition" }, viewQueries: [{ propertyName: "dynamicComponent", first: true, predicate: DynamicComponentComponent, descendants: true }], ngImport: i0, template: "<c8y-dynamic-component\n class=\"d-block p-l-16 p-r-16\"\n [componentId]=\"definition.id\"\n mode=\"config\"\n [config]=\"widgetConfigService.currentConfig$ | async\"\n></c8y-dynamic-component>\n", dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "ngmodule", type: DynamicComponentModule }, { kind: "component", type: i2.DynamicComponentComponent, selector: "c8y-dynamic-component", inputs: ["componentId", "config", "mode", "notFoundError", "executeResolvers"], outputs: ["updateWidgetClasses"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigGeneralComponent, decorators: [{
type: Component,
args: [{ standalone: true, imports: [AsyncPipe, DynamicComponentModule], selector: 'c8y-widget-config-general', template: "<c8y-dynamic-component\n class=\"d-block p-l-16 p-r-16\"\n [componentId]=\"definition.id\"\n mode=\"config\"\n [config]=\"widgetConfigService.currentConfig$ | async\"\n></c8y-dynamic-component>\n" }]
}], propDecorators: { definition: [{
type: Input
}], dynamicComponent: [{
type: ViewChild,
args: [DynamicComponentComponent]
}] } });
class WidgetConfigService {
/**
* The current configuration of the selected widget.
* Only to read the value. Use `updateConfig` to update the configuration.
**/
get currentConfig() {
return this._currentConfig$.value;
}
constructor(widgetConfigSectionService) {
this.widgetConfigSectionService = widgetConfigSectionService;
this._currentConfig$ = new BehaviorSubject(null);
/**
* The preview of the currently selected widget. If `null` no preview is shown
* (except if the selected widget has no configuration).
*/
this.preview$ = new BehaviorSubject(null);
/**
* The currently selected widget definition.
*/
this.selected$ = new BehaviorSubject(null);
/**
* The currently selected widget component.
*/
this.selectedComponent$ = new BehaviorSubject(null);
/**
* The current configuration of the selected widget as observable.
* Only to read the value. Use `updateConfig` to update the configuration.
*/
this.currentConfig$ = this._currentConfig$.asObservable();
/**
* Returns all the current sections.
*
* A configuration section is a component that can be used to configure a widget.
* Sections should be added via the `hookWidgetConfig`.
*/
this.currentSections$ = this.selected$.pipe(tap$1(() => (this.beforeSaveCallbacks = [])), withLatestFrom(this._currentConfig$), mergeMap$1(([selectedWidget, config]) => selectedWidget === null
? of([])
: this.widgetConfigSectionService.items$.pipe(map$1(sections => [
...this.getWidgetDefaultSections(selectedWidget, config),
...sections.filter(section => section.widgetId === selectedWidget.id)
]))), mergeMap$1((sections) => {
if (sections.length === 0) {
return Promise.resolve([]);
}
return Promise.all(sections.map(async (section) => ({
component: await section.loadComponent(),
label: section.label,
priority: section.priority || 0,
expanded: section.expanded || false,
initialState: section.initialState,
injector: section.injector,
providers: section.providers
})));
}), map$1(sections => sortByPriority(sections)), shareReplay$1(1));
/**
* Returns a list of all unique providers used in the current sections.
*/
this.providers$ = this.currentSections$.pipe(map$1(sections => Array.from(new Set(sections.flatMap(section => section.providers || [])))));
/**
* Indicates if the current selected widget has a configuration or not.
*/
this.hasConfig$ = this.currentSections$.pipe(map$1(sections => sections.length > 0));
/**
* The view state of the preview. Can be one of:
* - `noPreview`: No preview is shown and an empty state is shown.
* - `configPreview`: The preview is shown with the view component of the selected widget.
* - `templatePreview`: The preview is shown with any template passed with setPreview method.
*/
this.previewViewState$ = combineLatest([this.hasConfig$, this.preview$]).pipe(map$1(([hasConfig, preview]) => {
if (preview === null && hasConfig) {
return 'noPreview';
}
else if (preview instanceof TemplateRef) {
return 'templatePreview';
}
else if (preview === true || !hasConfig) {
return 'configPreview';
}
}));
this.STASH_CONFIG_DELAY = 300;
this.configChange$ = new Subject();
this.configChangeReset$ = new Subject();
this.beforeSaveCallbacks = [];
const add = value => state => ({ ...state, ...value });
const clear = () => _state => ({});
merge(this.configChange$.pipe(map$1(add)), this.configChangeReset$.pipe(map$1(clear)))
.pipe(scan((state, innerFn) => innerFn(state), {}), debounceTime(this.STASH_CONFIG_DELAY))
.subscribe(config => {
if (!this._currentConfig$.value) {
return;
}
Object.assign(this._currentConfig$.value, config);
if (this.selectedComponent$.value) {
this.selectedComponent$.value.emitConfigChange(this._currentConfig$.value);
}
});
}
/**
* Use this function to get a typed configuration of the selected widget.
* @returns The current configuration of the selected widget or an empty object if no config is defined.
*/
getWidgetConfig() {
return this.currentConfig$.pipe(map$1(current => {
return (current.config || {});
}));
}
/**
* Adds a callback which can check the configuration before saving.
*/
addOnBeforeSave(callback) {
this.beforeSaveCallbacks.push(callback);
}
/**
* Returns true if the configuration can be saved.
*/
canSave() {
if (this.beforeSaveCallbacks.length === 0) {
return Promise.resolve(true);
}
const config = this._currentConfig$.value;
return combineLatest(this.beforeSaveCallbacks.map(cb => {
const result = cb(config);
if (isObservable(result)) {
return result;
}
if (isPromise(result)) {
return from(result);
}
return of(result);
}))
.pipe(map$1(results => results.every(result => result === true)))
.toPromise();
}
/**
* Updates the configuration of the selected widget.
* @param change The change to apply to the configuration.
* @param force If set to true, the configuration will be updated even if no changes are made. Also concurrency checks are ignored and no change is emitted on the component.
* Use with caution and only if you want to reset the configuration to some default.
*/
updateConfig(change, force = false) {
if (force) {
this._currentConfig$.next({ ...this._currentConfig$.value, ...change });
return;
}
this.configChange$.next(change);
}
/**
* Used to initialize the configuration of the selected widget.
* @param config The configuration to set.
*/
initConfig(config) {
this._currentConfig$.next(config);
}
/**
* Select a widget
* @param selected The widget to select.
*/
selectWidget(selected) {
this.selected$.next(selected);
}
/**
* Deselect the currently selected.
*/
deselectWidget() {
this.selected$.next(null);
this.preview$.next(null);
this.configChangeReset$.next();
this._currentConfig$.next(null);
}
/**
* Enables a preview.
* @param preview If set to true, the view component of the current widget will be taken to render the preview.
* If set to a DynamicComponentDefinition, the preview will be rendered with the given component.
* If set to a TemplateRef, the preview will be rendered with the given template.
*/
setPreview(preview) {
this.preview$.next(preview);
}
/**
* By default, no sections are added to the configuration. However, to ensure
* backwards compatibility with old plugins, we check if the default asset selector
* and config section should be shown.
* @param selectedWidget The currently selected widget.
* @param config The used widget configuration.
* @returns The default sections.
*/
getWidgetDefaultSections(selectedWidget, config) {
const defaultWidgetConfigSections = [];
if (this.isDefaultSettingSectionNeeded(selectedWidget)) {
defaultWidgetConfigSections.push({
widgetId: selectedWidget.id,
label: gettext('Settings'),
loadComponent: () => Promise.resolve(WidgetConfigGeneralComponent),
initialState: {
definition: selectedWidget
},
priority: 0,
expanded: true
});
}
// Adds a default device selector for old plugins
if (this.isDefaultDeviceSelectorNeeded(selectedWidget, config)) {
defaultWidgetConfigSections.push({
widgetId: selectedWidget.id,
label: gettext('Asset selection'),
loadComponent: () => Promise.resolve(WidgetAssetSelectorComponent),
priority: 100,
initialState: {
isRequired: !config.settings?.ng1?.options?.deviceTargetNotRequired &&
!config.settings.deviceTargetNotRequired
}
});
}
return defaultWidgetConfigSections;
}
/**
* Sections should not be added by default anymore. But to ensure backwards compatibility, we
* check if the default selector should be shown. We assume that a default selector will be shown
* when:
* 1. The widget has a dynamic component for configuration (which is also deprecated).
* OR
* 2. The widget has not disabled the default selector.
*/
isDefaultDeviceSelectorNeeded(selectedWidget, config) {
if (!selectedWidget.loadConfigComponent && !selectedWidget.configComponent) {
return false;
}
return (!config.settings?.noDeviceTarget &&
!config.settings?.ng1?.options?.noDeviceTarget &&
!config.settings?.hideTarget);
}
/**
* Checks if a default setting section is needed.
* @param selectedWidget The currently selected widget.
* @returns true if one is needed.
*/
isDefaultSettingSectionNeeded(selectedWidget) {
if (selectedWidget.data?.settings?.upgrade) {
return (selectedWidget.data?.settings?.configComponent ||
selectedWidget.data?.settings?.configTemplateUrl);
}
return selectedWidget.configComponent || selectedWidget.loadConfigComponent;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigService, deps: [{ token: WidgetConfigSectionService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: WidgetConfigSectionService }] });
class WidgetConfigRootComponent {
constructor() {
this.widgetConfigService = inject(WidgetConfigService);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigRootComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetConfigRootComponent, isStandalone: true, selector: "c8y-widget-config-root", ngImport: i0, template: "<ng-container\n *ngFor=\"let section of widgetConfigService.currentSections$ | async\"\n>\n <c8y-widget-config-section [section]=\"section\"></c8y-widget-config-section>\n</ng-container>\n", dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: WidgetConfigSectionComponent, selector: "c8y-widget-config-section", inputs: ["section"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigRootComponent, decorators: [{
type: Component,
args: [{ standalone: true, imports: [NgFor, WidgetConfigSectionComponent, AsyncPipe], selector: 'c8y-widget-config-root', template: "<ng-container\n *ngFor=\"let section of widgetConfigService.currentSections$ | async\"\n>\n <c8y-widget-config-section [section]=\"section\"></c8y-widget-config-section>\n</ng-container>\n" }]
}] });
class WidgetService {
constructor(dynamicComponentService, translateService, moduleConfig) {
this.dynamicComponentService = dynamicComponentService;
this.translateService = translateService;
this.moduleConfig = moduleConfig;
this.dynamicComponentService.items$.subscribe(widgets => {
this.widgets = widgets;
});
}
async getWidgetDefinition(componentId) {
return this.dynamicComponentService.getById(componentId);
}
getWidgetDefinitions() {
const translatedComponents = this.widgets.map(cmp => ({
...cmp,
label: this.translateService.instant(cmp.label)
}));
// cloneDeep and injector lead to a performance issue. We removing the injectors here,
// as they are not needed, to avoid use of a lot of CPU for cloning.
const translatedComponentsWithoutInjector = translatedComponents.map(cmp => omit(cmp, 'injector'));
const filterFn = this.moduleConfig?.widgetFilter ||
(({ data }) => !(data && data.settings && data.settings.noNewWidgets));
return cloneDeep(sortBy(translatedComponentsWithoutInjector, 'label').filter(filterFn));
}
async mapLegacy(widget) {
const cmp = await this.getWidgetDefinition(widget.componentId || widget.name);
widget = this.mapDisplaySettings(widget, cmp);
if (get(cmp, 'data.settings.upgrade')) {
widget.widgetComponent = cmp.data.settings.widgetComponent;
widget.configComponent = cmp.data.settings.configComponent;
widget.templateUrl = cmp.data.settings.templateUrl;
widget.configTemplateUrl = cmp.data.settings.configTemplateUrl;
widget.transformConfigWithContext =
cmp.data.settings.componentTransformConfigWithContext ||
cmp.data.settings.transformConfigWithContext ||
widget.transformConfigWithContext;
}
else {
delete widget.templateUrl;
delete widget.configTemplateUrl;
}
return widget;
}
mapDisplaySettings(widget, cmp) {
if (cmp?.data?.displaySettings) {
widget.config = { displaySettings: cmp.data.displaySettings, ...(widget.config || {}) };
}
return widget;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetService, deps: [{ token: i2.DynamicComponentService }, { token: i2$1.TranslateService }, { token: CONTEXT_DASHBOARD_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i2.DynamicComponentService }, { type: i2$1.TranslateService }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [CONTEXT_DASHBOARD_CONFIG]
}] }] });
class WidgetPreviewComponent {
constructor() {
this.widgetConfigService = inject(WidgetConfigService);
this.previewClasses = {
'dashboard-theme-light': true,
'panel-title-regular': true
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetPreviewComponent, isStandalone: true, selector: "c8y-widget-preview", inputs: { previewClasses: "previewClasses" }, ngImport: i0, template: "<fieldset class=\"c8y-fieldset p-l-24 p-r-24 p-t-16 p-b-16\">\n <legend>{{ 'Preview' | translate }}</legend>\n <c8y-dashboard\n class=\"dashboard-preview-slot\"\n [columns]=\"1\"\n [ngClass]=\"previewClasses\"\n >\n <c8y-dashboard-child\n [title]=\"(widgetConfigService.selected$ | async)?.data?.title\"\n [isFrozen]=\"true\"\n [width]=\"1\"\n [height]=\"6\"\n >\n <c8y-dashboard-child-title>\n <span translate>{{ (widgetConfigService.selected$ | async)?.data?.title }}</span>\n </c8y-dashboard-child-title>\n\n <div\n class=\"fit-h d-flex d-col j-c-center a-i-center\"\n *ngIf=\"(widgetConfigService.previewViewState$ | async) === 'noPreview'\"\n >\n <c8y-ui-empty-state\n [icon]=\"'visibility-off'\"\n [title]=\"'No preview available.' | translate\"\n [subtitle]=\"'This widget does not provide any preview.' | translate\"\n [horizontal]=\"false\"\n ></c8y-ui-empty-state>\n </div>\n\n <c8y-dynamic-component\n *ngIf=\"(widgetConfigService.previewViewState$ | async) === 'configPreview'\"\n [componentId]=\"(widgetConfigService.selected$ | async)?.id\"\n [config]=\"(widgetConfigService.currentConfig$ | async)?.config\"\n ></c8y-dynamic-component>\n\n <ng-template\n *ngIf=\"(widgetConfigService.previewViewState$ | async) === 'templatePreview'\"\n [ngTemplateOutlet]=\"widgetConfigService.preview$ | async\"\n ></ng-template>\n </c8y-dashboard-child>\n </c8y-dashboard>\n</fieldset>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }, { kind: "ngmodule", type: CoreModule }, { kind: "component", type: i2.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: i2.DynamicComponentComponent, selector: "c8y-dynamic-component", inputs: ["componentId", "config", "mode", "notFoundError", "executeResolvers"], outputs: ["updateWidgetClasses"] }, { kind: "component", type: i2.DashboardComponent, selector: "c8y-dashboard", inputs: ["columns", "gap", "rows"], outputs: ["dashboardChange"] }, { kind: "component", type: i2.DashboardChildComponent, selector: "c8y-dashboard-child", inputs: ["x", "y", "width", "height", "data", "margin", "useIntersection", "isFrozen", "canToggleFullscreen", "editMode", "class"], outputs: ["changeStart", "changeEnd", "toggleFullscreen"] }, { kind: "component", type: i2.DashboardChildTitleComponent, selector: "c8y-dashboard-child-title" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetPreviewComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-widget-preview', standalone: true, imports: [NgIf, NgClass, C8yTranslatePipe, CommonModule, CoreModule], template: "<fieldset class=\"c8y-fieldset p-l-24 p-r-24 p-t-16 p-b-16\">\n <legend>{{ 'Preview' | translate }}</legend>\n <c8y-dashboard\n class=\"dashboard-preview-slot\"\n [columns]=\"1\"\n [ngClass]=\"previewClasses\"\n >\n <c8y-dashboard-child\n [title]=\"(widgetConfigService.selected$ | async)?.data?.title\"\n [isFrozen]=\"true\"\n [width]=\"1\"\n [height]=\"6\"\n >\n <c8y-dashboard-child-title>\n <span translate>{{ (widgetConfigService.selected$ | async)?.data?.title }}</span>\n </c8y-dashboard-child-title>\n\n <div\n class=\"fit-h d-flex d-col j-c-center a-i-center\"\n *ngIf=\"(widgetConfigService.previewViewState$ | async) === 'noPreview'\"\n >\n <c8y-ui-empty-state\n [icon]=\"'visibility-off'\"\n [title]=\"'No preview available.' | translate\"\n [subtitle]=\"'This widget does not provide any preview.' | translate\"\n [horizontal]=\"false\"\n ></c8y-ui-empty-state>\n </div>\n\n <c8y-dynamic-component\n *ngIf=\"(widgetConfigService.previewViewState$ | async) === 'configPreview'\"\n [componentId]=\"(widgetConfigService.selected$ | async)?.id\"\n [config]=\"(widgetConfigService.currentConfig$ | async)?.config\"\n ></c8y-dynamic-component>\n\n <ng-template\n *ngIf=\"(widgetConfigService.previewViewState$ | async) === 'templatePreview'\"\n [ngTemplateOutlet]=\"widgetConfigService.preview$ | async\"\n ></ng-template>\n </c8y-dashboard-child>\n </c8y-dashboard>\n</fieldset>\n" }]
}], propDecorators: { previewClasses: [{
type: Input
}] } });
class AppearanceSettingsComponent {
constructor() {
this.themeClass = 'dashboard-theme-light';
this.headerClass = 'panel-title-regular';
this.themeClassChange = new EventEmitter();
this.headerClassChange = new EventEmitter();
this.onChange = new EventEmitter();
this.possibleStylingTheme = DASHBOARD_THEME_CLASSES;
this.possibleStylingHeader = WIDGET_HEADER_CLASSES;
this.columns = 1;
this.dashboardDefaultLabel = gettext('Dashboard default');
}
themeClassClick(value) {
value = this.resetToDefault(value, this.themeClass, this.defaultThemeClass);
this.themeClass = value;
this.themeClassChange.emit(value);
this.onChange.emit();
}
headerClassClick(value) {
value = this.resetToDefault(value, this.headerClass, this.defaultHeaderClass);
this.headerClass = value;
this.headerClassChange.emit(value);
this.onChange.emit();
}
resetToDefault(value, compareTo, defaultValue) {
if (defaultValue && value === compareTo) {
value = defaultValue;
}
return value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppearanceSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: AppearanceSettingsComponent, isStandalone: true, selector: "c8y-appearance-settings", inputs: { themeClass: "themeClass", headerClass: "headerClass", defaultThemeClass: "defaultThemeClass", defaultHeaderClass: "defaultHeaderClass", dashboardSettings: "dashboardSettings", possibleStylingTheme: "possibleStylingTheme", possibleStylingHeader: "possibleStylingHeader", columns: "columns" }, outputs: { themeClassChange: "themeClassChange", headerClassChange: "headerClassChange", onChange: "onChange" }, ngImport: i0, template: "<div class=\"row\">\n <div class=\"col-lg-{{ 12 / columns }} col-xs-12\">\n <fieldset class=\"c8y-fieldset c8y-fieldset--lg\">\n <legend *ngIf=\"dashboardSettings\">{{ 'Default theme' | translate }}</legend>\n <legend *ngIf=\"!dashboardSettings\">{{ 'Theme' | translate }}</legend>\n <ul class=\"list-group\">\n <li\n class=\"list-group-item d-flex a-i-center p-l-0 p-r-0 fit-w\"\n *ngFor=\"let themeClassItem of possibleStylingTheme; let i = index\"\n >\n <div\n class=\"list-item-checkbox\"\n style=\"max-width: calc(100% - 24px)\"\n >\n <label class=\"c8y-radio\">\n <input\n name=\"content\"\n type=\"radio\"\n [id]=\"'groupradiocontentclass' + i\"\n [value]=\"themeClassItem.class\"\n [ngModel]=\"themeClass\"\n (click)=\"themeClassClick(themeClassItem.class)\"\n />\n <span></span>\n <span\n class=\"text-truncate\"\n title=\"{{ themeClassItem.label | translate }}{{\n themeClassItem.class === defaultThemeClass\n ? ' | ' + (dashboardDefaultLabel | translate)\n : ''\n }}\"\n >\n <span>{{ themeClassItem.label | translate }}</span>\n <br />\n <small\n class=\"text-muted\"\n *ngIf=\"themeClassItem.class === defaultThemeClass\"\n >\n {{ dashboardDefaultLabel | translate }}\n </small>\n </span>\n </label>\n </div>\n\n <button\n class=\"btn-help btn-help--sm m-l-auto\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ themeClassItem.description | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </li>\n </ul>\n </fieldset>\n </div>\n <div class=\"col-lg-{{ 12 / columns }} col-xs-12\">\n <fieldset class=\"c8y-fieldset c8y-fieldset--lg\">\n <legend *ngIf=\"dashboardSettings\">\n {{ 'Default widget header style' | translate }}\n </legend>\n <legend *ngIf=\"!dashboardSettings\">\n {{ 'Widget header style' | translate }}\n </legend>\n <ul class=\"list-group\">\n <li\n class=\"list-group-item d-flex a-i-center p-l-0 p-r-0\"\n *ngFor=\"let headerClassItem of possibleStylingHeader; let i = index\"\n >\n <div\n class=\"list-item-checkbox\"\n style=\"max-width: calc(100% - 24px)\"\n >\n <label class=\"c8y-radio\">\n <input\n name=\"header\"\n type=\"radio\"\n [id]=\"'groupradioheaderclass' + i\"\n [value]=\"headerClassItem.class\"\n [ngModel]=\"headerClass\"\n (click)=\"headerClassClick(headerClassItem.class)\"\n />\n <span></span>\n <span\n class=\"text-truncate\"\n title=\"{{ headerClassItem.label | translate }}{{\n headerClassItem.class === defaultHeaderClass\n ? ' | ' + (dashboardDefaultLabel | translate)\n : ''\n }}\"\n >\n <span>{{ headerClassItem.label | translate }}</span>\n <br />\n <small\n class=\"text-muted\"\n *ngIf=\"headerClassItem.class === defaultHeaderClass\"\n >\n {{ dashboardDefaultLabel | translate }}\n </small>\n </span>\n </label>\n </div>\n <button\n class=\"btn-help btn-help--sm m-l-auto\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ headerClassItem.description | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </li>\n </ul>\n </fieldset>\n </div>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i2$2.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AppearanceSettingsComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-appearance-settings', standalone: true, imports: [NgIf, NgForOf, FormsModule, PopoverModule, C8yTranslatePipe], template: "<div class=\"row\">\n <div class=\"col-lg-{{ 12 / columns }} col-xs-12\">\n <fieldset class=\"c8y-fieldset c8y-fieldset--lg\">\n <legend *ngIf=\"dashboardSettings\">{{ 'Default theme' | translate }}</legend>\n <legend *ngIf=\"!dashboardSettings\">{{ 'Theme' | translate }}</legend>\n <ul class=\"list-group\">\n <li\n class=\"list-group-item d-flex a-i-center p-l-0 p-r-0 fit-w\"\n *ngFor=\"let themeClassItem of possibleStylingTheme; let i = index\"\n >\n <div\n class=\"list-item-checkbox\"\n style=\"max-width: calc(100% - 24px)\"\n >\n <label class=\"c8y-radio\">\n <input\n name=\"content\"\n type=\"radio\"\n [id]=\"'groupradiocontentclass' + i\"\n [value]=\"themeClassItem.class\"\n [ngModel]=\"themeClass\"\n (click)=\"themeClassClick(themeClassItem.class)\"\n />\n <span></span>\n <span\n class=\"text-truncate\"\n title=\"{{ themeClassItem.label | translate }}{{\n themeClassItem.class === defaultThemeClass\n ? ' | ' + (dashboardDefaultLabel | translate)\n : ''\n }}\"\n >\n <span>{{ themeClassItem.label | translate }}</span>\n <br />\n <small\n class=\"text-muted\"\n *ngIf=\"themeClassItem.class === defaultThemeClass\"\n >\n {{ dashboardDefaultLabel | translate }}\n </small>\n </span>\n </label>\n </div>\n\n <button\n class=\"btn-help btn-help--sm m-l-auto\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ themeClassItem.description | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </li>\n </ul>\n </fieldset>\n </div>\n <div class=\"col-lg-{{ 12 / columns }} col-xs-12\">\n <fieldset class=\"c8y-fieldset c8y-fieldset--lg\">\n <legend *ngIf=\"dashboardSettings\">\n {{ 'Default widget header style' | translate }}\n </legend>\n <legend *ngIf=\"!dashboardSettings\">\n {{ 'Widget header style' | translate }}\n </legend>\n <ul class=\"list-group\">\n <li\n class=\"list-group-item d-flex a-i-center p-l-0 p-r-0\"\n *ngFor=\"let headerClassItem of possibleStylingHeader; let i = index\"\n >\n <div\n class=\"list-item-checkbox\"\n style=\"max-width: calc(100% - 24px)\"\n >\n <label class=\"c8y-radio\">\n <input\n name=\"header\"\n type=\"radio\"\n [id]=\"'groupradioheaderclass' + i\"\n [value]=\"headerClassItem.class\"\n [ngModel]=\"headerClass\"\n (click)=\"headerClassClick(headerClassItem.class)\"\n />\n <span></span>\n <span\n class=\"text-truncate\"\n title=\"{{ headerClassItem.label | translate }}{{\n headerClassItem.class === defaultHeaderClass\n ? ' | ' + (dashboardDefaultLabel | translate)\n : ''\n }}\"\n >\n <span>{{ headerClassItem.label | translate }}</span>\n <br />\n <small\n class=\"text-muted\"\n *ngIf=\"headerClassItem.class === defaultHeaderClass\"\n >\n {{ dashboardDefaultLabel | translate }}\n </small>\n </span>\n </label>\n </div>\n <button\n class=\"btn-help btn-help--sm m-l-auto\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ headerClassItem.description | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </li>\n </ul>\n </fieldset>\n </div>\n <ng-content></ng-content>\n</div>\n" }]
}], propDecorators: { themeClass: [{
type: Input
}], headerClass: [{
type: Input
}], defaultThemeClass: [{
type: Input
}], defaultHeaderClass: [{
type: Input
}], dashboardSettings: [{
type: Input
}], themeClassChange: [{
type: Output
}], headerClassChange: [{
type: Output
}], onChange: [{
type: Output
}], possibleStylingTheme: [{
type: Input
}], possibleStylingHeader: [{
type: Input
}], columns: [{
type: Input
}] } });
class WidgetConfigComponent {
get isEdit() {
return !!this.current;
}
get isDeviceTypeDashboard() {
return !!this.mo.c8y_Dashboard?.deviceType && !!this.mo.c8y_Dashboard?.deviceTypeValue;
}
constructor(widgetService, bottomDrawerRef, contextDashboardService, widgetConfigService) {
this.widgetService = widgetService;
this.bottomDrawerRef = bottomDrawerRef;
this.contextDashboardService = contextDashboardService;
this.widgetConfigService = widgetConfigService;
this.searchChange$ = new Subject();
this.searchTerm = '';
this.styling = {
headerClass: 'panel-title-regular',
contentClass: 'panel-content-light'
};
this.defaultStyling = {
headerClass: 'panel-title-regular',
contentClass: 'panel-content-light'
};
this.isUpgrade = false;
this.possibleStyling = { WIDGET_HEADER_CLASSES, WIDGET_CONTENT_CLASSES };
/**
* Used as wrapper component to inject providers.
*/
this.widgetConfigRoot = WidgetConfigRootComponent;
this.result = new Promise((resolve, reject) => {
this._save = resolve;
this._cancel = reject;
});
}
async ngAfterContentInit() {
this.components = this.widgetService.getWidgetDefinitions();
if (this.selected) {
this.current = clone(this.selected);
this.select(this.selected, this.isEdit ? 'config' : 'select');
}
this.searchSub = this.searchChange$
.pipe(switchMap((event) => iif(() => event.which !== 13, timer(200), timer(10))))
.subscribe(() => {
this.search();
});
this.setStylings();
}
isSaveDisabled() {
// If new section concept, the widget settings cannot be used to disable the device target.
// Use the WidgetConfigAssetTargetComponent instead with the initialState set to isRequired.
const isNewSectionConcept = !this.selected?.loadConfigComponent && !this.selected?.configComponent;
return this.configForm?.invalid || (!isNewSectionConcept && this.checkIfDeviceRequired());
}
checkIfDeviceRequired() {
const widgetConfig = this.widgetConfigService.currentConfig;
return (!widgetConfig ||
(!widgetConfig.settings?.deviceTargetNotRequired &&
!widgetConfig.device &&
!widgetConfig.settings?.noDeviceTarget));
}
async save() {
const canSave = await this.widgetConfigService.canSave();
if (!canSave) {
return;
}
const { _x, _y, _width, _height } = this.selected.data;
if (this.widgetConfig &&
this.widgetConfig.device &&
// serializing will be handled by resolver
!this.selected?.resolve?.device) {
const { id, name } = this.widgetConfig.device;
this.widgetConfig.device = { id, name };
}
this.widgetConfig = {
...this.widgetConfig,
...this.widgetConfigService.currentConfig
};
const widget = {
_x,
_y,
_width,
_height,
config: omit(this.widgetConfig, ['settings', 'displaySettings']),
title: this.selected.data.title,
componentId: this.selected.id,
id: this.isEdit ? this.current.data.id : String(Math.random()).substr(2),
classes: this.getStyle(),
...(!this.isEdit ? this.widgetConfig.settings.widgetDefaults : {})
};
this._save(widget);
await this.bottomDrawerRef.close();
this.widgetConfigService.deselectWidget();
}
select(cmp, mode = 'config') {
if (mode === 'config' && this.configForm) {
const formGroup = this.configForm.control;
for (const control of Object.keys(formGroup.controls)) {
formGroup.removeControl(control);
}
}
cmp.data = cmp.data || {};
this.selected = cmp;
this.isUpgrade = !!get(cmp, 'data.settings.upgrade');
this.contextDashboardService.formDisabled = this.isUpgrade;
if (this.isEdit) {
const { _x, _y, _width, _height, classes, title } = this.current.data;
this.selected.data = { ...this.selected.data, _x, _y, _width, _height, classes, title };
}
this.widgetConfig = cloneDeep(this.composeWidgetConfig(this.selected, this.context));
this.selected.data.title = this.selected.data.title || cmp.label;
this.componentLabel = cmp.label;
this.widgetConfigService.initConfig(this.widgetConfig);
this.widgetConfigService.selectWidget(this.selected);
}
search() {
if (this.searchTerm.length > 0) {
this.searchResult = this.components.filter(cmp => new RegExp(escapeRegExp(this.searchTerm.trim()), 'i').test(cmp.label));
}
else {
this.resetSearch();
}
}
resetSearch() {
this.searchTerm = '';
this.searchResult = undefined;
}
backToWidgetSelection() {
this.selected = null;
this.widgetConfigService.deselectWidget();
}
async close() {
this._cancel();
await this.bottomDrawerRef.close();
this.widgetConfigService.deselectWidget();
}
getStyle(isPreview = false) {
const cssClasses = {};
if (isPreview || !this.isDashboardDefaultStyle(this.styling.headerClass)) {
cssClasses[this.styling.headerClass] = true;
}
if (isPreview || !this.isDashboardDefaultStyle(this.styling.contentClass)) {
cssClasses[this.styling.contentClass] = true;
}
if (isPreview) {
cssClasses[`dashboard-theme-${this.defaultStyling.contentClass.split('-').pop()}`] = true;
}
return cssClasses;
}
ngOnDestroy() {
this.contextDashboardService.formDisabled = true;
if (this.searchSub) {
this.searchSub.unsubscribe();
}
}
setStylings() {
const dashboardClasses = (this.mo.c8y_Dashboard?.classes && Object.keys(this.mo.c8y_Dashboard.classes)) || [];
const dashboardWidgetClasses = (this.mo.c8y_Dashboard?.widgetClasses && Object.keys(this.mo.c8y_Dashboard.widgetClasses)) ||
[];
const widgetClasses = this.isEdit ? Object.keys(this.current.data.classes) : [];
this.styling = this.setDefaultStyle([
...dashboardClasses,
...dashboardWidgetClasses,
...widgetClasses
]);
this.defaultStyling = this.setDefaultStyle([...dashboardClasses, ...dashboardWidgetClasses]);
}
isDashboardDefaultStyle(className) {
const allClasses = {
...this.mo.c8y_Dashboard.classes,
...this.mo.c8y_Dashboard.widgetClasses
};
const styles = Object.keys(allClasses).map(cssClass => ({ class: cssClass }));
const style = this.contextDashboardService.getStyling(styles, className.split('-').pop(), undefined);
return !!style;
}
setDefaultStyle(setClasses) {
let contentClass = this.styling.contentClass;
let headerClass = this.styling.headerClass;
const styles = this.contextDashboardService
.getFilteredDashboardStyles(setClasses)
.map(c => c.split('-').pop());
styles.forEach(styleName => {
contentClass = this.contextDashboardService.getStyling(WIDGET_CONTENT_CLASSES, styleName, contentClass);
headerClass = this.contextDashboardService.getStyling(WIDGET_HEADER_CLASSES, styleName, headerClass);
});
return { headerClass, contentClass };
}
composeWidgetConfig(selectedComponent, context = {}) {
const setting = {
settings: {
...selectedComponent.data.settings,
...get(selectedComponent.data.settings, 'ng1.options'),
...get(selectedComponent.data, 'ng1.options'),
context,
dashboardMo: this.mo.c8y_Dashboard
},
...selectedComponent.data.config
};
return this.applyTargetIfDeviceDashboard(setting);
}
applyTargetIfDeviceDashboard(widgetConfig) {
const isDeviceType = this.contextDashboardService.isDeviceType(this.mo);
const noDeviceTarget = widgetConfig.settings.ng1
? widgetConfig.settings.ng1.options.noDeviceTarget
: widgetConfig.settings.noDeviceTarget;
if (isDeviceType) {
widgetConfig.settings.hideTarget = isDeviceType;
if (!noDeviceTarget) {
widgetConfig.device = {
id: this.context.id,
name: this.context.name
};
}
}
else if (this.context?.id && !noDeviceTarget) {
// if the widget is a new widget, we assign the current context
// as well to have the device available on init of the component
widgetConfig.device = {
id: this.context.id,
name: this.context.name
};
}
return widgetConfig;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigComponent, deps: [{ token: WidgetService }, { token: i2.BottomDrawerRef }, { token: ContextDashboardService }, { token: WidgetConfigService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetConfigComponent, isStandalone: true, selector: "c8y-widget-config", host: { classAttribute: "d-contents" }, viewQueries: [{ propertyName: "configForm", first: true, predicate: ["configForm"], descendants: true }], ngImport: i0, template: "<!-- select widget -->\n<ng-container *ngIf=\"!(widgetConfigService.selected$ | async)\">\n <div class=\"card-header j-c-center separator\">\n <div class=\"h4 text-center\">\n {{ 'Select widget' | translate }}\n </div>\n </div>\n <div class=\"card-inner-scroll fit-h bg-level-2\">\n <div\n class=\"bg-level-0 p-l-24 p-r-24 p-t-8 p-b-8 sticky-header-top-0 elevation-md\"\n style=\"z-index: 2\"\n >\n <div class=\"row\">\n <div class=\"col-sm-6 col-sm-offset-3\">\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Search' | translate\"\n placeholder=\"{{ 'Search\u2026' | translate }}\"\n type=\"text\"\n data-cy=\"widget-config--Search\"\n [(ngModel)]=\"searchTerm\"\n [ngModelOptions]=\"{ standalone: true }\"\n (keydown)=\"searchChange$.next($event)\"\n />\n <span class=\"input-group-btn\">\n <button\n class=\"btn btn-dot\"\n title=\"{{ 'Search' | translate }}\"\n type=\"button\"\n (click)=\"resetSearch()\"\n >\n <i [c8yIcon]=\"searchTerm.length === 0 ? 'search' : 'close'\"></i>\n </button>\n </span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"card-block\">\n <div class=\"card-group p-l-24 p-r-24 d-grid grid__col--auto-300 gap-24 card-select m-b-0\">\n <button\n class=\"btn-clean d-col card m-b-0\"\n [title]=\"cmp.description || cmp.label | translate\"\n type=\"button\"\n data-cy=\"widget-config--widget-list\"\n *ngFor=\"let cmp of searchResult || components\"\n (click)=\"select(cmp)\"\n >\n <div\n class=\"border-bottom\"\n role=\"presentation\"\n >\n <ng-container *ngIf=\"!cmp.previewImage; else previewImage\">\n <div class=\"h1\"><i c8yIcon=\"file-image-o\"></i></div>\n <small translate>Preview not available</small>\n </ng-container>\n <ng-template #previewImage>\n <img\n class=\"widget-thumbnail\"\n alt=\"{{ cmp.label | translate }}\"\n [src]=\"cmp.previewImage\"\n />\n </ng-template>\n </div>\n <div class=\"card-block\">\n <p class=\"card-title text-truncate text-medium\">\n <c8y-highlight\n text=\"{{ cmp.label | translate }}\"\n [pattern]=\"searchTerm\"\n ></c8y-highlight>\n </p>\n <p\n class=\"small text-default\"\n style=\"white-space: wrap\"\n >\n {{ cmp.description | translate }}\n </p>\n </div>\n </button>\n <c8y-ui-empty-state\n class=\"p-24 grid__col--fullspan\"\n [icon]=\"'search'\"\n [title]=\"'No widgets found.' | translate\"\n [subtitle]=\"' Rephrase your search term.' | translate\"\n *ngIf=\"searchResult && searchResult.length === 0\"\n >\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Reset search' | translate }}\"\n type=\"button\"\n (click)=\"resetSearch()\"\n >\n {{ 'Reset search' | translate }}\n </button>\n </c8y-ui-empty-state>\n </div>\n </div>\n </div>\n <div class=\"card-footer text-center separator flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n data-cy=\"widget-config--cancel-widget\"\n (click)=\"close()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n </div>\n</ng-container>\n\n<!-- widget configuration -->\n<ng-container *ngIf=\"!!(widgetConfigService.selected$ | async)\">\n <div class=\"card-header d-block separator-bottom flex-no-shrink\">\n <div\n class=\"h3 p-t-16\"\n title=\"{{ selected?.label | translate }}\"\n >\n <span>{{ selected?.label | translate }}</span>\n <button\n class=\"btn btn-default btn-sm m-l-8\"\n [title]=\"'Change widget' | translate\"\n (click)=\"backToWidgetSelection(); (false)\"\n >\n <i c8yIcon=\"replace\"></i>\n {{ 'Change widget' | translate }}\n </button>\n </div>\n <div\n class=\"p-t-8\"\n *ngIf=\"selected\"\n >\n <p>\n {{ selected.description | translate }}\n </p>\n </div>\n </div>\n\n <div class=\"d-grid grid__col--560-auto min-height-0 flex-grow\">\n <div class=\"bg-level-1 inner-scroll\">\n <div class=\"p-16 flex-no-shrink separator-bottom bg-level-1\">\n <c8y-form-group>\n <label\n for=\"widgetTitle\"\n translate\n >\n Widget title\n </label>\n <input\n class=\"form-control\"\n id=\"widgetTitle\"\n placeholder=\"{{ 'e.g.' | translate }} {{ componentLabel | translate }}\"\n name=\"title\"\n type=\"text\"\n required\n [(ngModel)]=\"selected?.data.title\"\n />\n </c8y-form-group>\n </div>\n\n <c8y-ui-empty-state\n class=\"p-24\"\n [icon]=\"'settings'\"\n [title]=\"'No configuration needed.' | translate\"\n [subtitle]=\"'This widget does not need any specific configuration.' | translate\"\n [horizontal]=\"true\"\n *ngIf=\"!(widgetConfigService.hasConfig$ | async)\"\n ></c8y-ui-empty-state>\n <div>\n <form\n name=\"form\"\n #configForm=\"ngForm\"\n >\n <ng-container *ngIf=\"widgetConfigService.hasConfig$ | async\">\n <ng-container *ngIf=\"widgetConfigService.providers$ | async as providers\">\n <ng-container\n *c8yComponentOutlet=\"widgetConfigRoot; providers: providers\"\n ></ng-container>\n </ng-container>\n </ng-container>\n </form>\n </div>\n </div>\n <div class=\"inner-scroll p-32 p-t-0\">\n <c8y-widget-preview [previewClasses]=\"getStyle(true)\"></c8y-widget-preview>\n\n <c8y-appearance-settings\n [(themeClass)]=\"styling.contentClass\"\n [(headerClass)]=\"styling.headerClass\"\n [possibleStylingTheme]=\"possibleStyling.WIDGET_CONTENT_CLASSES\"\n [possibleStylingHeader]=\"possibleStyling.WIDGET_HEADER_CLASSES\"\n [defaultThemeClass]=\"defaultStyling.contentClass\"\n [defaultHeaderClass]=\"defaultStyling.headerClass\"\n [columns]=\"2\"\n ></c8y-appearance-settings>\n </div>\n </div>\n\n <div class=\"card-footer separator text-center\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n data-cy=\"widget-config--cancel-widget\"\n (click)=\"close()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"submit\"\n data-cy=\"widget-config--save-widget\"\n (click)=\"save()\"\n [disabled]=\"(contextDashboardService.formDisabled$ | async) || isSaveDisabled()\"\n c8yProductExperience\n [actionName]=\"current ? 'editWidget' : 'createWidget'\"\n [actionData]=\"{ widgetName: selected && selected.id }\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n</ng-container>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i5.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i5.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i5.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: HighlightComponent, selector: "c8y-highlight", inputs: ["pattern", "text", "elementClass", "shouldTrimPattern"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: C8yComponentOutlet, selector: "[c8yComponentOutlet]", inputs: ["c8yComponentOutlet", "c8yComponentOutletInjector", "c8yComponentOutletEnvironmentInjector", "c8yComponentOutletProviders", "c8yComponentOutletInitialState"] }, { kind: "component", type: WidgetPreviewComponent, selector: "c8y-widget-preview", inputs: ["previewClasses"] }, { kind: "component", type: AppearanceSettingsComponent, selector: "c8y-appearance-settings", inputs: ["themeClass", "headerClass", "defaultThemeClass", "defaultHeaderClass", "dashboardSettings", "possibleStylingTheme", "possibleStylingHeader", "columns"], outputs: ["themeClassChange", "headerClassChange", "onChange"] }, { kind: "directive", type: ProductExperienceDirective, selector: "[c8yProductExperience]", inputs: ["actionName", "actionData", "inherit", "suppressDataOverriding"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-widget-config', host: { class: 'd-contents' }, imports: [
NgIf,
FormsModule,
IconDirective,
NgFor,
C8yTranslateDirective,
HighlightComponent,
EmptyStateComponent,
FormGroupComponent,
RequiredInputPlaceholderDirective,
C8yComponentOutlet,
WidgetPreviewComponent,
AppearanceSettingsComponent,
ProductExperienceDirective,
C8yTranslatePipe,
AsyncPipe
], template: "<!-- select widget -->\n<ng-container *ngIf=\"!(widgetConfigService.selected$ | async)\">\n <div class=\"card-header j-c-center separator\">\n <div class=\"h4 text-center\">\n {{ 'Select widget' | translate }}\n </div>\n </div>\n <div class=\"card-inner-scroll fit-h bg-level-2\">\n <div\n class=\"bg-level-0 p-l-24 p-r-24 p-t-8 p-b-8 sticky-header-top-0 elevation-md\"\n style=\"z-index: 2\"\n >\n <div class=\"row\">\n <div class=\"col-sm-6 col-sm-offset-3\">\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Search' | translate\"\n placeholder=\"{{ 'Search\u2026' | translate }}\"\n type=\"text\"\n data-cy=\"widget-config--Search\"\n [(ngModel)]=\"searchTerm\"\n [ngModelOptions]=\"{ standalone: true }\"\n (keydown)=\"searchChange$.next($event)\"\n />\n <span class=\"input-group-btn\">\n <button\n class=\"btn btn-dot\"\n title=\"{{ 'Search' | translate }}\"\n type=\"button\"\n (click)=\"resetSearch()\"\n >\n <i [c8yIcon]=\"searchTerm.length === 0 ? 'search' : 'close'\"></i>\n </button>\n </span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"card-block\">\n <div class=\"card-group p-l-24 p-r-24 d-grid grid__col--auto-300 gap-24 card-select m-b-0\">\n <button\n class=\"btn-clean d-col card m-b-0\"\n [title]=\"cmp.description || cmp.label | translate\"\n type=\"button\"\n data-cy=\"widget-config--widget-list\"\n *ngFor=\"let cmp of searchResult || components\"\n (click)=\"select(cmp)\"\n >\n <div\n class=\"border-bottom\"\n role=\"presentation\"\n >\n <ng-container *ngIf=\"!cmp.previewImage; else previewImage\">\n <div class=\"h1\"><i c8yIcon=\"file-image-o\"></i></div>\n <small translate>Preview not available</small>\n </ng-container>\n <ng-template #previewImage>\n <img\n class=\"widget-thumbnail\"\n alt=\"{{ cmp.label | translate }}\"\n [src]=\"cmp.previewImage\"\n />\n </ng-template>\n </div>\n <div class=\"card-block\">\n <p class=\"card-title text-truncate text-medium\">\n <c8y-highlight\n text=\"{{ cmp.label | translate }}\"\n [pattern]=\"searchTerm\"\n ></c8y-highlight>\n </p>\n <p\n class=\"small text-default\"\n style=\"white-space: wrap\"\n >\n {{ cmp.description | translate }}\n </p>\n </div>\n </button>\n <c8y-ui-empty-state\n class=\"p-24 grid__col--fullspan\"\n [icon]=\"'search'\"\n [title]=\"'No widgets found.' | translate\"\n [subtitle]=\"' Rephrase your search term.' | translate\"\n *ngIf=\"searchResult && searchResult.length === 0\"\n >\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Reset search' | translate }}\"\n type=\"button\"\n (click)=\"resetSearch()\"\n >\n {{ 'Reset search' | translate }}\n </button>\n </c8y-ui-empty-state>\n </div>\n </div>\n </div>\n <div class=\"card-footer text-center separator flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n data-cy=\"widget-config--cancel-widget\"\n (click)=\"close()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n </div>\n</ng-container>\n\n<!-- widget configuration -->\n<ng-container *ngIf=\"!!(widgetConfigService.selected$ | async)\">\n <div class=\"card-header d-block separator-bottom flex-no-shrink\">\n <div\n class=\"h3 p-t-16\"\n title=\"{{ selected?.label | translate }}\"\n >\n <span>{{ selected?.label | translate }}</span>\n <button\n class=\"btn btn-default btn-sm m-l-8\"\n [title]=\"'Change widget' | translate\"\n (click)=\"backToWidgetSelection(); (false)\"\n >\n <i c8yIcon=\"replace\"></i>\n {{ 'Change widget' | translate }}\n </button>\n </div>\n <div\n class=\"p-t-8\"\n *ngIf=\"selected\"\n >\n <p>\n {{ selected.description | translate }}\n </p>\n </div>\n </div>\n\n <div class=\"d-grid grid__col--560-auto min-height-0 flex-grow\">\n <div class=\"bg-level-1 inner-scroll\">\n <div class=\"p-16 flex-no-shrink separator-bottom bg-level-1\">\n <c8y-form-group>\n <label\n for=\"widgetTitle\"\n translate\n >\n Widget title\n </label>\n <input\n class=\"form-control\"\n id=\"widgetTitle\"\n placeholder=\"{{ 'e.g.' | translate }} {{ componentLabel | translate }}\"\n name=\"title\"\n type=\"text\"\n required\n [(ngModel)]=\"selected?.data.title\"\n />\n </c8y-form-group>\n </div>\n\n <c8y-ui-empty-state\n class=\"p-24\"\n [icon]=\"'settings'\"\n [title]=\"'No configuration needed.' | translate\"\n [subtitle]=\"'This widget does not need any specific configuration.' | translate\"\n [horizontal]=\"true\"\n *ngIf=\"!(widgetConfigService.hasConfig$ | async)\"\n ></c8y-ui-empty-state>\n <div>\n <form\n name=\"form\"\n #configForm=\"ngForm\"\n >\n <ng-container *ngIf=\"widgetConfigService.hasConfig$ | async\">\n <ng-container *ngIf=\"widgetConfigService.providers$ | async as providers\">\n <ng-container\n *c8yComponentOutlet=\"widgetConfigRoot; providers: providers\"\n ></ng-container>\n </ng-container>\n </ng-container>\n </form>\n </div>\n </div>\n <div class=\"inner-scroll p-32 p-t-0\">\n <c8y-widget-preview [previewClasses]=\"getStyle(true)\"></c8y-widget-preview>\n\n <c8y-appearance-settings\n [(themeClass)]=\"styling.contentClass\"\n [(headerClass)]=\"styling.headerClass\"\n [possibleStylingTheme]=\"possibleStyling.WIDGET_CONTENT_CLASSES\"\n [possibleStylingHeader]=\"possibleStyling.WIDGET_HEADER_CLASSES\"\n [defaultThemeClass]=\"defaultStyling.contentClass\"\n [defaultHeaderClass]=\"defaultStyling.headerClass\"\n [columns]=\"2\"\n ></c8y-appearance-settings>\n </div>\n </div>\n\n <div class=\"card-footer separator text-center\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n data-cy=\"widget-config--cancel-widget\"\n (click)=\"close()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Save' | translate }}\"\n type=\"submit\"\n data-cy=\"widget-config--save-widget\"\n (click)=\"save()\"\n [disabled]=\"(contextDashboardService.formDisabled$ | async) || isSaveDisabled()\"\n c8yProductExperience\n [actionName]=\"current ? 'editWidget' : 'createWidget'\"\n [actionData]=\"{ widgetName: selected && selected.id }\"\n >\n {{ 'Save' | translate }}\n </button>\n </div>\n</ng-container>\n" }]
}], ctorParameters: () => [{ type: WidgetService }, { type: i2.BottomDrawerRef }, { type: ContextDashboardService }, { type: WidgetConfigService }], propDecorators: { configForm: [{
type: ViewChild,
args: ['configForm', { static: false }]
}] } });
/**
* The context dashboard is a dashboard which resolves it data from the current context (device or group)
* it is displayed on. It usually uses the route.data for it, but you can pass
* a different managedObject to the [mo] input parameter to change that behavior.
*/
class ContextDashboardComponent {
get disabled() {
return !this.hasPermissionToEditDashboard || !this.dashboardDetails?.isCollapsed;
}
constructor(route, router, contextDashboardService, alert, renderer, moduleConfig, widgetService, bottomDrawerService, gainsightService, actionBarService, translateService, modal, editModeService) {
this.route = route;
this.router = router;
this.contextDashboardService = contextDashboardService;
this.alert = alert;
this.renderer = renderer;
this.moduleConfig = moduleConfig;
this.widgetService = widgetService;
this.bottomDrawerService = bottomDrawerService;
this.gainsightService = gainsightService;
this.actionBarService = actionBarService;
this.translateService = translateService;
this.modal = modal;
this.editModeService = editModeService;
this.childrenClasses = '';
this.setTitle = false;
this.defaultWidgets = [];
this._canCopy = true;
this.canDelete = true;
this.isLoading = true;
this.showContextHelpButton = true;
/**
* Hides dashboard availability selection.
*/
this.hideAvailability = false;
this.class = '';
this.widgets = [];
this.canCopy = true;
this.dashboardTypeLabel = gettext('Dashboard for "{{ dashboardType }}"');
this.dashboardTypePopover = gettext(`
The layout and configuration of this dashboard is used by all other assets of model type
<strong>{{ dashboardType }}</strong>. Any changes made to this dashboard instance will be applied to all
other instances.`);
this.hasPermissionToEditDashboard = false;
this.destroy$ = new Subject();
}
ngOnInit() {
if (!this.name) {
this.loadContextDashboard();
return;
}
this.loadNamedDashboard();
}
async beforeUnloadHandler($event) {
const canDeactivate = await this.canDeactivate(true);
if (!canDeactivate) {
$event.returnValue = true;
}
}
/**
* Applies the current context to the widget
* @param widget The widget to apply the context to.
*/
applyDeviceTarget(widget) {
if (widget.config.device) {
widget.config.device = { id: this.context.id, name: this.context.name };
}
}
/**
* Removes the route listener.
*/
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
if (this.dataSub) {
this.dataSub.unsubscribe();
}
}
/**
* Guards component from unintended navigation away or closing tab without saving changes.
* Navigation continues if true is returned, and navigation is cancelled if returned value is false.
* @param omitConfirm Omits confirm calls inside method body (without this param, when method is used in
* on 'beforeunload' event handler, error is thrown in console because confirm is blocked by browser anyway and returns false)
*/
async canDeactivate(omitConfirm = false) {
const canDeactivate = (await this.dashboardDetails.canDeactivate(omitConfirm)) &&
(await this.widgetsDashboard.canDeactivate(omitConfirm));
// Needed in order to reset edit mode, otherwise when switching between dashboards, the edit mode is still enabled.
if (canDeactivate) {
await this.cancelEditMode(true);
}
return canDeactivate;
}
/**
* Restores the dashboard widgets to the default widgets.
*/
async restore() {
const mesg = gettext('You are about to reset the widgets of this dashboard. All changes to the dashboard widgets will get lost and cannot be recovered. Do you want to proceed?');
try {
await this.modal.confirm(gettext('Reset widgets'), mesg, Status.WARNING, {
ok: gettext('Reset`dashboard`'),
cancel: gettext('Cancel')
});
this.isLoading = true;
this.mo.c8y_Dashboard.children = this.contextDashboardService.mapWidgets(this.defaultWidgets);
this.mo.c8y_Dashboard.historyDescription = { changeType: 'reset' };
await this.contextDashboardService.update(this.mo);
await this.onLoad();
}
catch (error) {
if (error) {
this.alert.addServerFailure(error);
}
}
finally {
this.isLoading = false;
}
}
/**
* Method called on every widgets dimensions or position change but also for adding new widget (because adding
* widget causes layout changes). It recognizes what type of change has been done and updates current widgets state
* accordingly.
* @param child Change object.
*/
positionOrWidgetDimensionChange(child) {
const currentState = this.editModeService.getCurrentState()?.children || {};
const descriptionProp = child.children.length === Object.values(currentState).length ? 'arrangement' : 'added';
let changeName;
let widgetsChanged;
if (descriptionProp === 'added') {
changeName = DASHBOARD_CHILDREN_STATE_NAME.added;
const addedWidget = child.children.find(c => !currentState[c.data.id]);
widgetsChanged = [addedWidget.data];
}
else {
changeName = DASHBOARD_CHILDREN_STATE_NAME.arrangement;
widgetsChanged = child.children
.map(c => {
const lastStateChild = currentState[c.data.id];
if (c.x != lastStateChild._x ||
c.y != lastStateChild._y ||
c.width != lastStateChild._width ||
c.height != lastStateChild._height) {
return c.data;
}
})
.filter(Boolean);
}
this.updateDashboardChildren(child, changeName, descriptionProp, widgetsChanged);
}
async revertChange(revertType) {
let dashboardChildren;
if (revertType === 'undo') {
dashboardChildren = this.editModeService.undo().children;
}
else {
dashboardChildren = this.editModeService.redo().children;
}
await this.setWidgets(dashboardChildren);
}
/**
* Updates all dashboards children's. Useful for position changes on the dashboard.
* @param child The child to change.
* @param changeName Name of the change to indicate it on undo/redo button.
* @param descriptionProp Property to add to dashboard change history.
* @param widgetsChanged List of changed widgets.
*/
updateDashboardChildren(child, changeName, descriptionProp, widgetsChanged) {
const { children } = child;
const mappedChildren = keyBy(children.map(c => this.componentToWidget(c)), 'id');
this.setNewState({ name: changeName, children: mappedChildren }, descriptionProp, widgetsChanged);
}
/**
* Copies the dashboard and current context to a clipboard.
*/
async copyDashboard() {
const viewContext = this.route.parent.snapshot.data?.context;
this.contextDashboardService.copyClipboard = {
dashboardId: this.mo.id,
dashboard: cloneDeep(this.mo.c8y_Dashboard),
context: cloneDeep({ context: viewContext, contextData: this.context })
};
if (viewContext) {
const ctx = viewContext.split('/').shift();
const msg = this.translateService.instant('Dashboard copied. Navigate to the desired {{ ctx }} and select "Paste dashboard"', { ctx });
this.alert.success(msg);
}
this.actionBarService.refresh();
}
editDashboard() {
const isEdit = !!this.mo.id;
this.dashboardDetails.show(isEdit);
}
/**
* Remove the complete dashboard and navigate away.
*/
async deleteDashboard() {
await this.contextDashboardService.delete(this.mo);
if (this.route.parent) {
const route = this.route.parent.snapshot.url.map(segment => segment.path).join('/');
this.router.navigateByUrl(route);
}
await this.onDeleteGSEvent();
}
get isDeviceTypeDashboard() {
return !!this.dashboard?.deviceType;
}
/**
* Edits a widget on the dashboard.
* @param change The widget change event.
*/
async editWidget(change) {
const { x, y, width, height } = change.source;
const component = await this.widgetService.getWidgetDefinition(change.widget.name || change.widget.componentId);
if (!component) {
this.addWidget();
return;
}
await this.addWidget({
...component,
data: { ...component.data, ...change.widget, _x: x, _y: y, _width: width, _height: height }
});
}
/**
* Adds a widget to the dashboard.
* @param selected Define a selected component to switch to edit mode directly.
*/
async addWidget(selected) {
const partialCloneSelected = selected
? { ...selected, data: cloneDeep(selected.data) }
: selected;
const activeContext = this.context.contextData ? this.context.contextData : this.context;
const initialState = {
mo: this.mo,
context: this.context.c8y_Report ? {} : activeContext,
selected: partialCloneSelected
};
const bottomDrawer = this.bottomDrawerService.openDrawer(WidgetConfigComponent, {
initialState,
closeOnNavigation: false,
disableClickOutside: true
});
try {
const newWidget = await bottomDrawer.instance.result;
if (!this.mo.c8y_Dashboard.children) {
this.mo.c8y_Dashboard.children = {};
}
newWidget.classes = this.mergeWidgetClasses(newWidget);
await this.updateWidget(newWidget);
// New state in edit mode is added only for widget update. When adding widget, layout change is always triggered,
// so it's easier to set new state on layout change only
if (selected) {
const children = {
...this.editModeService.getCurrentState().children,
[newWidget.id]: newWidget
};
this.setNewState({ name: DASHBOARD_CHILDREN_STATE_NAME.config, children }, 'config', [
newWidget
]);
}
}
catch (ex) {
// intended empty
}
}
async saveWidgetsToDashboard() {
const currentState = this.editModeService.getCurrentState();
const editedChildren = currentState.children;
// first-time save (no `id` yet): create the dashboard, then immediately update it with the edited widgets
if (!this.mo.id) {
this.mo = await this.contextDashboardService.create(this.mo.c8y_Dashboard, this.context, this.name);
}
this.mo.c8y_Dashboard.children = editedChildren;
this.mo.c8y_Dashboard.historyDescription = {
changeType: 'update',
widgetChanges: this.mapStateToHistoryDescription(currentState.changeHistory)
};
await this.contextDashboardService.update(this.mo);
this.editModeService.reset();
this.editModeService.init({
name: DASHBOARD_CHILDREN_STATE_NAME.initial,
children: editedChildren,
changeHistory: {}
});
this.isCopyDisabled = this.getDashboardCopyPermissionState();
}
async cancelEditMode(onDeactivate = false) {
const dashboardChildren = this.editModeService.reset();
if (this.dashboard) {
this.dashboard.historyDescription = {};
}
this.widgetsDashboard.editMode$.next(false);
if (!onDeactivate) {
// when setWidgets is called during navigation from device to device, navigation fails
await this.setWidgets(dashboardChildren.children);
}
}
/**
* Updates a widget or adds a new one if it doesn't exist on
* the dashboard.
* @param widget The new widget
*/
async updateWidget(widget) {
const index = findIndex(this.widgets, { id: widget.id });
const isNew = index === -1;
const mappedWidget = await this.widgetService.mapLegacy(widget);
if (isNew) {
this.widgets.push(mappedWidget);
}
else {
this.widgets.splice(index, 1, mappedWidget);
}
}
/**
* Removes a widget and rearranges the remaining ones
* if necessary.
* @param change The change event.
*/
async deleteWidget(change) {
try {
const { widget, source } = change;
const removed = this.widgets.find(({ id }) => id === widget.id);
this.widgets.splice(this.widgets.indexOf(removed), 1);
const { dashboard } = source;
dashboard.children = dashboard.children.filter(c => c.data.id !== widget.id);
// using setTimeout to give the component the chance to remove it.
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout));
await sleep(0);
const child = new DashboardChildChange(source);
child.collapseUpAll();
this.updateDashboardChildren(child, DASHBOARD_CHILDREN_STATE_NAME.removed, 'removed', [
widget
]);
}
catch (e) {
this.alert.addServerFailure(e);
}
}
/**
* This is a workaround to ensure that the dragged-element
* (which is attached to the body) has the right styling.
*/
addDashboardClassToBody() {
this.class.split(' ').forEach(cssClass => {
this.renderer.addClass(document.body, cssClass);
});
}
/**
* This is a workaround to ensure that the dragged-element
* (which is attached to the body) has the right styling.
*/
removeDashboardClassFromBody() {
this.class.split(' ').forEach(cssClass => {
this.renderer.removeClass(document.body, cssClass);
});
}
async onLoad(trackExperience) {
this.isNamedDashboard = this.contextDashboardService.isNamed(this.mo);
this.hasPermissionToEditDashboard = this.dashboard
? await this.contextDashboardService.canEditDashboard(this.mo)
: false;
this.canCopy =
this._canCopy &&
(this.contextDashboardService.isDeviceDashboard(this.mo) ||
this.contextDashboardService.isDeviceType(this.mo) ||
this.contextDashboardService.isGroupDashboard(this.mo));
const dashboardChildren = cloneDeep(this.mo.c8y_Dashboard?.children);
const dashboardClasses = {
'c8y-grid-dashboard': true,
dashboard: true,
...this.dashboard?.classes
};
this.isCopyDisabled = this.dashboard
? this.getDashboardCopyPermissionState()
: { state: false, reason: null };
this.editModeService.init({
name: gettext('Initial state'),
children: dashboardChildren,
changeHistory: {}
});
await this.setWidgets(dashboardChildren, trackExperience);
this.class = Object.keys(dashboardClasses).join(' ');
if (this.isReport) {
this.addReportDashboardSettings();
}
this.isLoading = false;
if (!this.dashboard) {
this.dashboardDetails.show(false);
}
else {
await this.onLoadGSEvent();
}
}
async dashboardPreview(layoutChanges) {
if (layoutChanges?.classes) {
const dashboardClasses = {
'c8y-grid-dashboard': true,
dashboard: true,
...layoutChanges.classes
};
this.class = Object.keys(dashboardClasses).join(' ');
}
else if (this.dashboard) {
this.dashboard.widgetClasses = {
...this.dashboard.widgetClasses,
...layoutChanges.widgetClasses
};
}
}
async reloadDashboard(dashboardMo) {
this.dashboard = dashboardMo.c8y_Dashboard;
this.mo = dashboardMo;
await this.onLoad();
}
async setWidgets(dashboardChildren, trackExperience = false) {
const isDeviceType = this.contextDashboardService.isDeviceType(this.mo);
this.widgets = await Promise.all(Object.values(dashboardChildren || {}).map(widget => {
widget.classes = this.mergeWidgetClasses(widget);
if (isDeviceType) {
this.applyDeviceTarget(widget);
}
if (trackExperience) {
this.gainsightService.triggerEvent('loadWidget', {
widgetName: widget.componentId || widget.name
});
}
return this.widgetService.mapLegacy(widget);
}));
}
loadContextDashboard() {
this.dataSub = this.route.data.subscribe(({ dashboard, isReport }) => {
this.context = this.route.parent.snapshot.data.contextData || {};
this.title = this.context?.name;
this.mo = dashboard;
this.dashboard = this.mo.c8y_Dashboard;
this.isReport = isReport || this.contextDashboardService.isReport(this.mo);
if (this.isReport && this.dashboard && !this.dashboard?.name) {
this.dashboard.name = this.title;
}
if (this.isReport && this.dashboard) {
this.dashboard.c8y_IsNavigatorNode = this.context.c8y_IsNavigatorNode;
}
this.patchSensorPhoneDashboard(this.dashboard, this.mo.owner, this.context?.type);
this.onLoad(true);
});
}
/**
* To enable translation for widgets within the dashboard, the "translateWidgetTitle" flag must be enabled.
* The property needs to be patched, as the "Sensor App" does not provide this setting.
*/
patchSensorPhoneDashboard(dashboard, owner, type) {
if (type === 'c8y_SensorPhone' && owner?.includes('device_phone')) {
dashboard.translateWidgetTitle = true;
}
}
loadNamedDashboard() {
this.dataSub = this.contextDashboardService
.getDashboard(this.name, this.defaultWidgets)
.subscribe(mo => {
this.context = this.context || {};
this.mo = mo;
this.dashboard = this.mo.c8y_Dashboard;
this.onLoad(true);
});
}
mergeWidgetClasses(widget) {
const hasHeaderClass = WIDGET_HEADER_CLASSES.find(el => widget.classes && widget.classes[el.class]);
const widgetClasses = hasHeaderClass
? { ...widget.classes }
: { ...this.dashboard.widgetClasses, ...widget.classes };
return {
card: true,
'card-dashboard': true,
[kebabCase(widget.componentId || widget.name)]: true,
...widgetClasses
};
}
componentToWidget(child) {
return {
...omit(child.data, ['componentTransformConfigWithContext', 'transformConfigWithContext']), // remove legacy
...{ _x: child.x, _y: child.y, _width: child.width, _height: child.height }
};
}
addReportDashboardSettings() {
this.setTitle = true;
this.title = this.context?.name || gettext('New report');
this.breadcrumbSettings = { icon: 'th', label: 'Reports', path: 'reports' };
this.canDelete = false;
}
getDashboardCopyPermissionState() {
if (!this.contextDashboardService.hasPermissionsToCopyDashboard()) {
return { state: false, reason: CopyDashboardDisabledReason.PERMISSIONS };
}
const allDashboardChildrenAreValid = every(this.mo.c8y_Dashboard.children, child => {
const config = child.config || {};
const dataPoints = config.datapoints || [];
return !((config.device && config.device.id !== this.context.id) ||
some(dataPoints, dataPoint => dataPoint.__target?.id !== this.context.id));
});
if (!allDashboardChildrenAreValid) {
return { state: false, reason: CopyDashboardDisabledReason.WRONG_REFERENCE };
}
return { state: true };
}
setNewState(state, descriptionProp, widgetsChanged) {
const description = this.getDescriptionForNewState(descriptionProp, widgetsChanged);
this.editModeService.newState({ ...state, changeHistory: description });
}
getDescriptionForNewState(descriptionProp, widgetsChanged) {
const changeHistory = this.editModeService.getCurrentState().changeHistory;
if (!changeHistory[descriptionProp]) {
changeHistory[descriptionProp] = {};
}
widgetsChanged.forEach(w => {
if (descriptionProp === 'removed' && changeHistory.added?.[w.id]) {
// if widget was added during current edit session and then removed, it should not be at description at all
delete changeHistory.added[w.id];
if (changeHistory.config?.[w.id]) {
delete changeHistory.config[w.id];
}
if (changeHistory.arrangement?.[w.id]) {
delete changeHistory?.arrangement[w.id];
}
}
else if ((descriptionProp === 'config' || descriptionProp === 'arrangement') &&
changeHistory.added?.[w.id]) {
// if widget was added and then modified in current edit session, it should only be indicated as added
}
else {
changeHistory[descriptionProp][w.id] = w;
}
});
return changeHistory;
}
mapStateToHistoryDescription(description) {
const historyDescription = {};
for (const [key, value] of Object.entries(description)) {
const widgets = Object.values(value);
if (!widgets.length) {
continue;
}
historyDescription[key] = Object.values(value).map(widget => widget.title);
}
return historyDescription;
}
async onDeleteGSEvent() {
const parentName = await this.convertStringToHash(this.context?.name);
const dashboardName = await this.convertStringToHash(this.dashboard?.name);
this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE.DASHBOARD.EVENTS.DASHBOARDS, {
component: PRODUCT_EXPERIENCE.DASHBOARD.COMPONENTS.DELETE_DASHBOARD,
action: PRODUCT_EXPERIENCE.DASHBOARD.ACTIONS.DELETE,
name: dashboardName,
id: this.mo.id,
nameId: `${dashboardName}_${this.mo.id}`,
isDashboardTemplate: this.mo.c8y_Dashboard.deviceType,
parentAssetId: this.context?.id ? this.context.id : 'noContext',
parentAssetName: this.context?.id ? parentName : 'noContext',
parentAssetType: this.context?.id ? this.context.type : 'noContext',
parentAssetNameId: this.context?.id ? `${parentName}_${this.context.id}` : 'noContext',
parentAssetNameDashboardName: this.context?.id
? `${parentName}_${dashboardName}`
: 'noContext',
parentAssetIdDashboardId: this.context?.id ? `${this.context.id}_${this.mo.id}` : 'noContext',
parentAssetNameDashboardId: this.context?.id ? `${parentName}_${this.mo.id}` : 'noContext',
parentAssetNameIdDashboardNameId: this.context?.id
? `${parentName}_${this.context.id}_${dashboardName}_${this.mo.id}`
: 'noContext',
dashboardType: this.dashboard.deviceType ? this.dashboard.deviceTypeValue : null,
context: this.contextDashboardService.getContextForGS(this.context)
});
}
async onLoadGSEvent() {
const parentName = await this.convertStringToHash(this.context?.name);
const dashboardName = (await this.convertStringToHash(this.dashboard?.name)) ??
this.extractDefaultDashboardName(this.mo) ??
'none';
this.gainsightService.triggerEvent(this.isReport
? PRODUCT_EXPERIENCE.DASHBOARD.EVENTS.REPORTS
: PRODUCT_EXPERIENCE.DASHBOARD.EVENTS.DASHBOARDS, {
component: PRODUCT_EXPERIENCE.DASHBOARD.COMPONENTS.DASHBOARD_VIEW,
action: PRODUCT_EXPERIENCE.DASHBOARD.ACTIONS.LOAD,
name: dashboardName,
id: this.mo.id,
isDashboardTemplate: this.mo.c8y_Dashboard.deviceType,
nameId: `${dashboardName}_${this.mo.id}`,
parentAssetId: this.context?.id ? this.context.id : 'noContext',
parentAssetName: this.context?.id ? parentName : 'noContext',
parentAssetType: this.context?.id ? this.context.type : 'noContext',
parentAssetNameId: this.context?.id ? `${parentName}_${this.context.id}` : 'noContext',
parentAssetNameDashboardName: this.context?.id
? `${parentName}_${dashboardName}`
: 'noContext',
parentAssetIdDashboardId: this.context?.id
? `${this.context.id}_${this.mo.id}`
: 'noContext',
parentAssetNameDashboardId: this.context?.id ? `${parentName}_${this.mo.id}` : 'noContext',
parentAssetNameIdDashboardNameId: this.context?.id
? `${parentName}_${this.context.id}_${dashboardName}_${this.mo.id}`
: 'noContext',
dashboardType: this.dashboard.deviceType ? this.dashboard.deviceTypeValue : null,
context: this.isReport
? PRODUCT_EXPERIENCE.DASHBOARD.CONTEXT.REPORT
: this.contextDashboardService.getContextForGS(this.context)
});
}
extractDefaultDashboardName(obj) {
const nameKey = Object.keys(obj).find(key => key.startsWith('c8y_Dashboard!name!'));
if (nameKey) {
return nameKey.split('!').pop();
}
return null;
}
async convertStringToHash(str) {
if (!str) {
return null;
}
return (await this.gainsightService.shouldSendPiiData())
? str
: this.gainsightService.hashGroupName(str);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardComponent, deps: [{ token: i1.ActivatedRoute }, { token: i1.Router }, { token: ContextDashboardService }, { token: i2.AlertService }, { token: i0.Renderer2 }, { token: CONTEXT_DASHBOARD_CONFIG }, { token: WidgetService }, { token: i2.BottomDrawerService }, { token: i2.GainsightService }, { token: i2.ActionBarService }, { token: i2$1.TranslateService }, { token: i2.ModalService }, { token: DashboardEditModeService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: ContextDashboardComponent, isStandalone: true, selector: "c8y-context-dashboard", inputs: { name: "name", childrenClasses: "childrenClasses", context: "context", setTitle: "setTitle", defaultWidgets: "defaultWidgets", _canCopy: ["canCopy", "_canCopy"], canDelete: "canDelete", isLoading: "isLoading", breadcrumbSettings: "breadcrumbSettings", showContextHelpButton: "showContextHelpButton", translateWidgetTitle: "translateWidgetTitle", hideAvailability: "hideAvailability" }, host: { listeners: { "window:beforeunload": "beforeUnloadHandler($event)" }, properties: { "class": "this.class" }, styleAttribute: "\n display: block;\n ", classAttribute: "dashboard c8y-grid-dashboard" }, viewQueries: [{ propertyName: "dashboardDetails", first: true, predicate: DashboardDetailComponent, descendants: true, static: true }, { propertyName: "widgetsDashboard", first: true, predicate: WidgetsDashboardComponent, descendants: true, static: true }], ngImport: i0, template: "<c8y-title *ngIf=\"title\">\n {{ title }}\n</c8y-title>\n\n<c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"10000\"\n *ngIf=\"dashboard?.deviceType && dashboard.deviceTypeValue\"\n>\n <button\n class=\"btn-clean btn-link\"\n popoverTitle=\"{{ 'Dashboard template' | translate }}\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"dashboardTypePopoverRef\"\n placement=\"bottom\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n >\n <span class=\"tag tag--info text-12\">\n <span class=\"dashboard-template-marker\"></span>\n {{ dashboardTypeLabel | translate: { dashboardType: dashboard.deviceTypeValue } }}\n </span>\n </button>\n <ng-template #dashboardTypePopoverRef>\n <div\n [innerHTML]=\"dashboardTypePopover | translate: { dashboardType: dashboard.deviceTypeValue }\"\n ></div>\n </ng-template>\n</c8y-action-bar-item>\n\n<c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"-1\"\n *ngIf=\"defaultWidgets.length > 0\"\n>\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Reset widgets' | translate }}\"\n type=\"button\"\n px-event=\"Reset dashboard\"\n (click)=\"restore()\"\n [disabled]=\"disabled || !mo?.id || (widgetsDashboard?.editMode$ | async)\"\n data-cy=\"context-dashboard--button-reset-dashboard\"\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"reset\"\n ></i>\n <span class=\"visible-xs-inline hidden-sm visible-md-inline visible-lg-inline\">\n {{ 'Reset widgets' | translate }}\n </span>\n </button>\n</c8y-action-bar-item>\n\n<c8y-help\n src=\"/docs/cockpit/working-with-dashboards/#working-with-dashboards\"\n *ngIf=\"showContextHelpButton\"\n></c8y-help>\n\n<c8y-dashboard-detail\n class=\"d-contents\"\n [isReport]=\"isReport\"\n [dashboard]=\"dashboard\"\n [mo]=\"mo\"\n [context]=\"context\"\n [deviceType]=\"context?.type\"\n [hideAvailability]=\"hideAvailability\"\n [isNamedDashboard]=\"isNamedDashboard\"\n (dashboardSaved)=\"reloadDashboard($event)\"\n (previewChanged)=\"dashboardPreview($event)\"\n></c8y-dashboard-detail>\n\n<c8y-widgets-dashboard\n [context]=\"context\"\n [contextDashboard]=\"dashboard\"\n [widgets]=\"widgets\"\n [isCopyDisabled]=\"isCopyDisabled\"\n [settings]=\"{\n isLoading: isLoading,\n isDisabled: disabled,\n canDelete: canDelete && !!dashboard,\n translateWidgetTitle: dashboard?.translateWidgetTitle ?? translateWidgetTitle,\n allowFullscreen: moduleConfig.allowFullscreen,\n title: setTitle ? dashboard?.name || title : undefined,\n widgetMargin: dashboard?.widgetMargin,\n canCopy: canCopy && !!dashboard,\n defaultWidth: dashboard?.columns >= 24 ? 8 : 4,\n columns: dashboard?.columns || 12\n }\"\n [breadcrumb]=\"breadcrumbSettings\"\n [editModeButtons]=\"{\n undoButtonDisabled: editModeService.undoButtonDisabled,\n changeToUndoName: editModeService.changeToUndoName,\n redoButtonDisabled: !editModeService.redoStackLastItem,\n changeToRedoName: editModeService.redoStackLastItem?.name\n }\"\n (onChangeDashboard)=\"positionOrWidgetDimensionChange($event)\"\n (onAddWidget)=\"addWidget()\"\n (onEditWidget)=\"editWidget($event)\"\n (onDeleteWidget)=\"deleteWidget($event)\"\n (onSaveDashboard)=\"saveWidgetsToDashboard()\"\n (onCancelDashboard)=\"cancelEditMode()\"\n (revertChange)=\"revertChange($event)\"\n (onChangeStart)=\"addDashboardClassToBody()\"\n (onChangeEnd)=\"removeDashboardClassFromBody()\"\n (onEditDashboard)=\"editDashboard()\"\n (onCopyDashboard)=\"copyDashboard()\"\n (onDeleteDashboard)=\"deleteDashboard()\"\n></c8y-widgets-dashboard>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "component", type: ActionBarItemComponent, selector: "c8y-action-bar-item", inputs: ["placement", "priority", "itemClass", "injector", "groupId", "inGroupPriority"] }, { kind: "directive", type: PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: HelpComponent, selector: "c8y-help", inputs: ["src", "isCollapsed", "priority", "icon"] }, { kind: "component", type: DashboardDetailComponent, selector: "c8y-dashboard-detail", inputs: ["isReport", "deviceType", "context", "mo", "dashboard", "isNamedDashboard", "hideAvailability"], outputs: ["dashboardSaved", "previewChanged"] }, { kind: "component", type: WidgetsDashboardComponent, selector: "c8y-widgets-dashboard", inputs: ["widgets", "context", "contextDashboard", "settings", "isCopyDisabled", "breadcrumb", "editModeButtons"], outputs: ["onAddWidget", "onEditWidget", "onDeleteWidget", "onChangeDashboard", "onResize", "onEditDashboard", "onCopyDashboard", "onDeleteDashboard", "onChangeStart", "onChangeEnd", "onSaveDashboard", "onCancelDashboard", "revertChange"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
__decorate([
memoize(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], ContextDashboardComponent.prototype, "convertStringToHash", null);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-context-dashboard', host: {
style: `
display: block;
`,
class: 'dashboard c8y-grid-dashboard'
}, imports: [
NgIf,
TitleComponent,
ActionBarItemComponent,
PopoverDirective,
IconDirective,
HelpComponent,
DashboardDetailComponent,
WidgetsDashboardComponent,
C8yTranslatePipe,
AsyncPipe
], template: "<c8y-title *ngIf=\"title\">\n {{ title }}\n</c8y-title>\n\n<c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"10000\"\n *ngIf=\"dashboard?.deviceType && dashboard.deviceTypeValue\"\n>\n <button\n class=\"btn-clean btn-link\"\n popoverTitle=\"{{ 'Dashboard template' | translate }}\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"dashboardTypePopoverRef\"\n placement=\"bottom\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n >\n <span class=\"tag tag--info text-12\">\n <span class=\"dashboard-template-marker\"></span>\n {{ dashboardTypeLabel | translate: { dashboardType: dashboard.deviceTypeValue } }}\n </span>\n </button>\n <ng-template #dashboardTypePopoverRef>\n <div\n [innerHTML]=\"dashboardTypePopover | translate: { dashboardType: dashboard.deviceTypeValue }\"\n ></div>\n </ng-template>\n</c8y-action-bar-item>\n\n<c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"-1\"\n *ngIf=\"defaultWidgets.length > 0\"\n>\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Reset widgets' | translate }}\"\n type=\"button\"\n px-event=\"Reset dashboard\"\n (click)=\"restore()\"\n [disabled]=\"disabled || !mo?.id || (widgetsDashboard?.editMode$ | async)\"\n data-cy=\"context-dashboard--button-reset-dashboard\"\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"reset\"\n ></i>\n <span class=\"visible-xs-inline hidden-sm visible-md-inline visible-lg-inline\">\n {{ 'Reset widgets' | translate }}\n </span>\n </button>\n</c8y-action-bar-item>\n\n<c8y-help\n src=\"/docs/cockpit/working-with-dashboards/#working-with-dashboards\"\n *ngIf=\"showContextHelpButton\"\n></c8y-help>\n\n<c8y-dashboard-detail\n class=\"d-contents\"\n [isReport]=\"isReport\"\n [dashboard]=\"dashboard\"\n [mo]=\"mo\"\n [context]=\"context\"\n [deviceType]=\"context?.type\"\n [hideAvailability]=\"hideAvailability\"\n [isNamedDashboard]=\"isNamedDashboard\"\n (dashboardSaved)=\"reloadDashboard($event)\"\n (previewChanged)=\"dashboardPreview($event)\"\n></c8y-dashboard-detail>\n\n<c8y-widgets-dashboard\n [context]=\"context\"\n [contextDashboard]=\"dashboard\"\n [widgets]=\"widgets\"\n [isCopyDisabled]=\"isCopyDisabled\"\n [settings]=\"{\n isLoading: isLoading,\n isDisabled: disabled,\n canDelete: canDelete && !!dashboard,\n translateWidgetTitle: dashboard?.translateWidgetTitle ?? translateWidgetTitle,\n allowFullscreen: moduleConfig.allowFullscreen,\n title: setTitle ? dashboard?.name || title : undefined,\n widgetMargin: dashboard?.widgetMargin,\n canCopy: canCopy && !!dashboard,\n defaultWidth: dashboard?.columns >= 24 ? 8 : 4,\n columns: dashboard?.columns || 12\n }\"\n [breadcrumb]=\"breadcrumbSettings\"\n [editModeButtons]=\"{\n undoButtonDisabled: editModeService.undoButtonDisabled,\n changeToUndoName: editModeService.changeToUndoName,\n redoButtonDisabled: !editModeService.redoStackLastItem,\n changeToRedoName: editModeService.redoStackLastItem?.name\n }\"\n (onChangeDashboard)=\"positionOrWidgetDimensionChange($event)\"\n (onAddWidget)=\"addWidget()\"\n (onEditWidget)=\"editWidget($event)\"\n (onDeleteWidget)=\"deleteWidget($event)\"\n (onSaveDashboard)=\"saveWidgetsToDashboard()\"\n (onCancelDashboard)=\"cancelEditMode()\"\n (revertChange)=\"revertChange($event)\"\n (onChangeStart)=\"addDashboardClassToBody()\"\n (onChangeEnd)=\"removeDashboardClassFromBody()\"\n (onEditDashboard)=\"editDashboard()\"\n (onCopyDashboard)=\"copyDashboard()\"\n (onDeleteDashboard)=\"deleteDashboard()\"\n></c8y-widgets-dashboard>\n" }]
}], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: i1.Router }, { type: ContextDashboardService }, { type: i2.AlertService }, { type: i0.Renderer2 }, { type: undefined, decorators: [{
type: Inject,
args: [CONTEXT_DASHBOARD_CONFIG]
}] }, { type: WidgetService }, { type: i2.BottomDrawerService }, { type: i2.GainsightService }, { type: i2.ActionBarService }, { type: i2$1.TranslateService }, { type: i2.ModalService }, { type: DashboardEditModeService }], propDecorators: { name: [{
type: Input
}], childrenClasses: [{
type: Input
}], context: [{
type: Input
}], setTitle: [{
type: Input
}], defaultWidgets: [{
type: Input
}], _canCopy: [{
type: Input,
args: ['canCopy']
}], canDelete: [{
type: Input
}], isLoading: [{
type: Input
}], breadcrumbSettings: [{
type: Input
}], showContextHelpButton: [{
type: Input
}], translateWidgetTitle: [{
type: Input
}], hideAvailability: [{
type: Input
}], class: [{
type: HostBinding,
args: ['class']
}], dashboardDetails: [{
type: ViewChild,
args: [DashboardDetailComponent, { static: true }]
}], widgetsDashboard: [{
type: ViewChild,
args: [WidgetsDashboardComponent, { static: true }]
}], beforeUnloadHandler: [{
type: HostListener,
args: ['window:beforeunload', ['$event']]
}], convertStringToHash: [] } });
class DashboardDetailsTabsFactory {
constructor() {
this.activatedRoute = inject(ActivatedRoute);
this.contextRouteService = inject(ContextRouteService);
this.generalTab = {
label: gettext('General'),
featureId: DashboardDetailsTabId.GENERAL,
icon: 'imac-settings',
priority: 100,
path: [
{
outlets: {
[DASHBOARD_DETAILS_OUTLET]: DashboardDetailsTabId.GENERAL
}
}
],
tabsOutlet: DASHBOARD_DETAILS_TABS_OUTLET_NAME
};
this.appearanceTab = {
label: gettext('Appearance'),
featureId: DashboardDetailsTabId.APPEARANCE,
icon: 'web-design',
priority: 50,
path: [
{
outlets: {
[DASHBOARD_DETAILS_OUTLET]: DashboardDetailsTabId.APPEARANCE
}
}
],
tabsOutlet: DASHBOARD_DETAILS_TABS_OUTLET_NAME
};
this.versionHistoryTab = {
label: gettext('Version history'),
featureId: DashboardDetailsTabId.VERSIONHISTORY,
icon: 'versions',
priority: 10,
path: [
{
outlets: {
[DASHBOARD_DETAILS_OUTLET]: DashboardDetailsTabId.VERSIONHISTORY
}
}
],
tabsOutlet: DASHBOARD_DETAILS_TABS_OUTLET_NAME
};
}
get() {
const rootContext = this.contextRouteService.getRootContextOfChild(this.activatedRoute);
if (rootContext === ViewContext.Dashboard) {
return [this.generalTab, this.appearanceTab, this.versionHistoryTab];
}
return [];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailsTabsFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailsTabsFactory }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDetailsTabsFactory, decorators: [{
type: Injectable
}] });
class PasteDashboardActionComponent {
constructor(vcRef, router, contextDashboardService, inventoryService) {
this.vcRef = vcRef;
this.router = router;
this.contextDashboardService = contextDashboardService;
this.inventoryService = inventoryService;
this.pasteDisabled = false;
this.dashboardName = '';
}
async ngOnInit() {
this.currentUrl = this.router.url;
this.contextId = this.currentUrl.match(/\d+/)[0];
const isDevice = new RegExp(/device\/\d+/).test(this.currentUrl);
const isGroup = new RegExp(/group\/\d+/).test(this.currentUrl);
this.currentViewContext = isDevice ? ViewContext.Device : isGroup ? ViewContext.Group : null;
await this.checkIfPasteIsAllowed();
this.vcRef.createEmbeddedView(this.templatePaste);
const clipboard = this.contextDashboardService.copyClipboard;
if (clipboard) {
this.dashboardName = clipboard.dashboard.name;
}
}
async pasteDashboard() {
if (!this.contextMO) {
this.contextMO = await this.getContextMo(this.contextId);
}
const context = {
context: this.currentViewContext,
contextData: this.contextMO
};
await this.contextDashboardService.pasteDashboard(context);
}
async getContextMo(id) {
return (await this.inventoryService.detail(id)).data;
}
async checkIfPasteIsAllowed() {
const clipboardViewContext = this.contextDashboardService.copyClipboard.context.context;
const isTypeDashboard = this.contextDashboardService.copyClipboard?.dashboard?.deviceType;
if (this.currentViewContext !== clipboardViewContext) {
if (clipboardViewContext === ViewContext.Device) {
this.buttonTitle = gettext('Device dashboards can only be copied into a device.');
}
else if (clipboardViewContext === ViewContext.Group) {
this.buttonTitle = gettext('Group dashboards can only be copied into a group.');
}
this.pasteDisabled = true;
return;
}
else if (isTypeDashboard) {
this.contextMO = await this.getContextMo(this.contextId);
const dashboardDeviceType = this.contextDashboardService.copyClipboard.dashboard.deviceTypeValue;
if (dashboardDeviceType === this.contextMO.type) {
this.buttonTitle = gettext('Dashboard template can only be copied into assets of a different type.');
this.pasteDisabled = true;
return;
}
}
this.buttonTitle = gettext('Paste dashboard');
this.pasteDisabled = false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PasteDashboardActionComponent, deps: [{ token: i0.ViewContainerRef }, { token: i1.Router }, { token: ContextDashboardService }, { token: i1$1.InventoryService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: PasteDashboardActionComponent, isStandalone: true, selector: "c8y-paste-dashboard-action", viewQueries: [{ propertyName: "templatePaste", first: true, predicate: ["templatePaste"], descendants: true, read: TemplateRef, static: true }], ngImport: i0, template: `
<ng-template #templatePaste>
<li>
<button
class="btn btn-link"
title="{{ buttonTitle | translate }}"
type="button"
(click)="pasteDashboard()"
[disabled]="pasteDisabled"
>
<i class="m-r-4" c8yIcon="clipboard"></i>
<span ngNonBindable translate [translateParams]="{ dashboardName: dashboardName }">
Paste dashboard "{{ dashboardName }}"
</span>
</button>
</li>
</ng-template>
`, isInline: true, dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PasteDashboardActionComponent, decorators: [{
type: Component,
args: [{
selector: 'c8y-paste-dashboard-action',
template: `
<ng-template #templatePaste>
<li>
<button
class="btn btn-link"
title="{{ buttonTitle | translate }}"
type="button"
(click)="pasteDashboard()"
[disabled]="pasteDisabled"
>
<i class="m-r-4" c8yIcon="clipboard"></i>
<span ngNonBindable translate [translateParams]="{ dashboardName: dashboardName }">
Paste dashboard "{{ dashboardName }}"
</span>
</button>
</li>
</ng-template>
`,
imports: [IconDirective, C8yTranslateDirective, C8yTranslatePipe]
}]
}], ctorParameters: () => [{ type: i0.ViewContainerRef }, { type: i1.Router }, { type: ContextDashboardService }, { type: i1$1.InventoryService }], propDecorators: { templatePaste: [{
type: ViewChild,
args: ['templatePaste', { read: TemplateRef, static: true }]
}] } });
class TypeDashboardInfoComponent {
constructor(inventory, alert) {
this.inventory = inventory;
this.alert = alert;
}
async ngOnInit() {
await this.updateDeviceTypeCount();
}
async updateDeviceTypeCount() {
try {
const count = await this.inventory.count({
type: this.deviceTypeValue || this.context.c8y_Dashboard.deviceTypeValue
});
this.deviceTypeInstancesCount = count.data;
this.asyncRenderSuccess?.();
}
catch (error) {
this.alert.addServerFailure(error);
this.asyncRenderFail?.();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeDashboardInfoComponent, deps: [{ token: i1$1.InventoryService }, { token: i2.AlertService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: TypeDashboardInfoComponent, isStandalone: true, selector: "c8y-type-dashboard-info", inputs: { deviceTypeValue: "deviceTypeValue", displayDeviceTypeValue: "displayDeviceTypeValue", context: "context", asyncRenderSuccess: "asyncRenderSuccess", asyncRenderFail: "asyncRenderFail" }, ngImport: i0, template: "<ul class=\"list-unstyled small animated fadeIn\">\n <li class=\"p-t-4 p-b-4 d-flex separator-top-bottom text-nowrap\">\n <label class=\"small m-b-0 m-r-8\">{{ 'Target asset model' | translate }}</label>\n <span class=\"m-l-auto\">\n <span class=\"label label-info\">\n {{ displayDeviceTypeValue || context.c8y_Dashboard.deviceTypeValue }}\n </span>\n </span>\n </li>\n <li class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\">\n <label class=\"small m-b-0 m-r-8\">{{ 'Dashboard instances' | translate }}</label>\n <span class=\"m-l-auto\">{{ deviceTypeInstancesCount }}</span>\n </li>\n <ng-container *ngIf=\"context?.c8y_Dashboard\">\n <li class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\">\n <label class=\"small m-b-0 m-r-8\">{{ 'Created' | translate }}</label>\n <span class=\"m-l-auto\">\n {{ context.creationTime | c8yDate }} {{ 'by`user`' | translate }} {{ context.owner }}\n </span>\n </li>\n <li\n class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\"\n *ngIf=\"context?.c8y_DashboardHistory?.length\"\n >\n <label class=\"small m-b-0 m-r-8\">{{ 'Last modified' | translate }}</label>\n <span class=\"m-l-auto\">\n {{ context.c8y_DashboardHistory[0].created | c8yDate }} {{ 'by`user`' | translate }}\n {{ context.c8y_DashboardHistory[0].author }}\n </span>\n </li>\n </ng-container>\n</ul>\n", dependencies: [{ kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeDashboardInfoComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-type-dashboard-info', standalone: true, imports: [C8yTranslatePipe, DatePipe, NgIf], template: "<ul class=\"list-unstyled small animated fadeIn\">\n <li class=\"p-t-4 p-b-4 d-flex separator-top-bottom text-nowrap\">\n <label class=\"small m-b-0 m-r-8\">{{ 'Target asset model' | translate }}</label>\n <span class=\"m-l-auto\">\n <span class=\"label label-info\">\n {{ displayDeviceTypeValue || context.c8y_Dashboard.deviceTypeValue }}\n </span>\n </span>\n </li>\n <li class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\">\n <label class=\"small m-b-0 m-r-8\">{{ 'Dashboard instances' | translate }}</label>\n <span class=\"m-l-auto\">{{ deviceTypeInstancesCount }}</span>\n </li>\n <ng-container *ngIf=\"context?.c8y_Dashboard\">\n <li class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\">\n <label class=\"small m-b-0 m-r-8\">{{ 'Created' | translate }}</label>\n <span class=\"m-l-auto\">\n {{ context.creationTime | c8yDate }} {{ 'by`user`' | translate }} {{ context.owner }}\n </span>\n </li>\n <li\n class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\"\n *ngIf=\"context?.c8y_DashboardHistory?.length\"\n >\n <label class=\"small m-b-0 m-r-8\">{{ 'Last modified' | translate }}</label>\n <span class=\"m-l-auto\">\n {{ context.c8y_DashboardHistory[0].created | c8yDate }} {{ 'by`user`' | translate }}\n {{ context.c8y_DashboardHistory[0].author }}\n </span>\n </li>\n </ng-container>\n</ul>\n" }]
}], ctorParameters: () => [{ type: i1$1.InventoryService }, { type: i2.AlertService }], propDecorators: { deviceTypeValue: [{
type: Input
}], displayDeviceTypeValue: [{
type: Input
}], context: [{
type: Input
}], asyncRenderSuccess: [{
type: Input
}], asyncRenderFail: [{
type: Input
}] } });
class WidgetPreviewWrapperComponent {
constructor() {
this.widgetConfigService = inject(WidgetConfigService);
}
set previewSet(template) {
if (template) {
this.widgetConfigService.setPreview(template);
return;
}
this.widgetConfigService.setPreview(null);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetPreviewWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetPreviewWrapperComponent, isStandalone: true, selector: "c8y-widget-preview-wrapper", viewQueries: [{ propertyName: "previewSet", first: true, predicate: ["previewContent"], descendants: true }], ngImport: i0, template: '<ng-template #previewContent><ng-content></ng-content></ng-template>', isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetPreviewWrapperComponent, decorators: [{
type: Component,
args: [{
selector: 'c8y-widget-preview-wrapper',
template: '<ng-template #previewContent><ng-content></ng-content></ng-template>',
standalone: true,
imports: []
}]
}], propDecorators: { previewSet: [{
type: ViewChild,
args: ['previewContent']
}] } });
const defaultConfig = {
allowFullscreen: true
};
/**
* Module for the Context Dashboard feature.
* This module provides components and configurations for managing dashboards,
* {@link ContextDashboardComponent} is the main component for displaying the dashboard context.
* {@link WidgetConfigComponent} allows for configuring widgets within the dashboard.
* {@link DashboardDetailComponent} provides detailed views of individual dashboards.
*/
class ContextDashboardModule {
static config(config = {}) {
return {
ngModule: ContextDashboardModule,
providers: [
{
provide: CONTEXT_DASHBOARD_CONFIG,
useValue: { ...defaultConfig, ...config }
},
hookRoute(config.routes || [])
]
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardModule, imports: [CoreModule,
TooltipModule,
PopoverModule,
AssetSelectorModule,
IconSelectorModule, i1$3.BsDropdownModule, i2$3.CollapseModule, RouterModule,
AppearanceSettingsComponent,
TypeDashboardInfoComponent,
WidgetPreviewComponent,
WidgetConfigSectionComponent,
WidgetPreviewWrapperComponent,
ContextDashboardComponent,
WidgetConfigComponent,
DashboardDetailComponent,
AddDashboardComponent,
PasteDashboardActionComponent], exports: [ContextDashboardComponent,
WidgetConfigComponent,
DashboardDetailComponent,
TypeDashboardInfoComponent,
WidgetConfigSectionComponent,
AppearanceSettingsComponent,
WidgetPreviewWrapperComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardModule, providers: [
{
provide: CONTEXT_DASHBOARD_CONFIG,
useValue: defaultConfig
},
hookTab(DashboardDetailsTabsFactory),
hookRoute([
{
path: DashboardDetailsTabId.GENERAL,
loadComponent: () => import('./c8y-ngx-components-context-dashboard-dashboard-general-settings.component-RXvOcs04.mjs').then(m => m.DashboardGeneralSettingsComponent),
outlet: DASHBOARD_DETAILS_OUTLET,
context: ViewContext.Dashboard
},
{
path: DashboardDetailsTabId.APPEARANCE,
loadComponent: () => import('./c8y-ngx-components-context-dashboard-dashboard-appearance-settings.component-C-JLbcn5.mjs').then(m => m.DashboardAppearanceSettingsComponent),
outlet: DASHBOARD_DETAILS_OUTLET,
context: ViewContext.Dashboard
},
{
path: DashboardDetailsTabId.VERSIONHISTORY,
loadComponent: () => import('./c8y-ngx-components-context-dashboard-dashboard-version-history.component-DmPIJGXO.mjs').then(m => m.DashboardVersionHistoryComponent),
outlet: DASHBOARD_DETAILS_OUTLET,
context: ViewContext.Dashboard
}
])
], imports: [CoreModule,
TooltipModule,
PopoverModule,
AssetSelectorModule,
IconSelectorModule,
BsDropdownModule.forRoot(),
CollapseModule.forRoot(),
RouterModule,
AppearanceSettingsComponent,
WidgetPreviewComponent,
WidgetConfigSectionComponent,
ContextDashboardComponent,
WidgetConfigComponent,
DashboardDetailComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContextDashboardModule, decorators: [{
type: NgModule,
args: [{
imports: [
CoreModule,
TooltipModule,
PopoverModule,
AssetSelectorModule,
IconSelectorModule,
BsDropdownModule.forRoot(),
CollapseModule.forRoot(),
RouterModule,
AppearanceSettingsComponent,
TypeDashboardInfoComponent,
WidgetPreviewComponent,
WidgetConfigSectionComponent,
WidgetPreviewWrapperComponent,
ContextDashboardComponent,
WidgetConfigComponent,
DashboardDetailComponent,
AddDashboardComponent,
PasteDashboardActionComponent
],
exports: [
ContextDashboardComponent,
WidgetConfigComponent,
DashboardDetailComponent,
TypeDashboardInfoComponent,
WidgetConfigSectionComponent,
AppearanceSettingsComponent,
WidgetPreviewWrapperComponent
],
providers: [
{
provide: CONTEXT_DASHBOARD_CONFIG,
useValue: defaultConfig
},
hookTab(DashboardDetailsTabsFactory),
hookRoute([
{
path: DashboardDetailsTabId.GENERAL,
loadComponent: () => import('./c8y-ngx-components-context-dashboard-dashboard-general-settings.component-RXvOcs04.mjs').then(m => m.DashboardGeneralSettingsComponent),
outlet: DASHBOARD_DETAILS_OUTLET,
context: ViewContext.Dashboard
},
{
path: DashboardDetailsTabId.APPEARANCE,
loadComponent: () => import('./c8y-ngx-components-context-dashboard-dashboard-appearance-settings.component-C-JLbcn5.mjs').then(m => m.DashboardAppearanceSettingsComponent),
outlet: DASHBOARD_DETAILS_OUTLET,
context: ViewContext.Dashboard
},
{
path: DashboardDetailsTabId.VERSIONHISTORY,
loadComponent: () => import('./c8y-ngx-components-context-dashboard-dashboard-version-history.component-DmPIJGXO.mjs').then(m => m.DashboardVersionHistoryComponent),
outlet: DASHBOARD_DETAILS_OUTLET,
context: ViewContext.Dashboard
}
])
]
}]
}] });
class DashboardActionBarFactory {
constructor() {
this.contextDashboardService = inject(ContextDashboardService);
}
get(activeRoute) {
const actions = [];
const data = !activeRoute.parent || activeRoute.snapshot.data.context
? activeRoute.snapshot.data
: activeRoute.parent.snapshot.data;
const { contextData } = data;
const canPaste = contextData &&
data.context === this.targetContext &&
this.contextDashboardService.copyClipboard;
if (canPaste) {
actions.push({
priority: -10,
placement: 'more',
template: PasteDashboardActionComponent
});
}
return actions;
}
}
class DeviceInfoDashboardComponent {
constructor(router, widgetService, translateService, inventoryService) {
this.router = router;
this.widgetService = widgetService;
this.translateService = translateService;
this.inventoryService = inventoryService;
this.isLoading = true;
this.dashboardName = '';
this.DEVICE_INFO_WIDGETS = [
{
name: 'deviceStatusWidget',
title: gettext('Device status'),
_x: 0,
_y: 0,
_height: 5,
_width: 8
},
{
name: 'Data points graph',
title: gettext('Device and communication'),
_x: 8,
_y: 0,
_height: 5,
_width: 16,
config: {
interval: 'hours',
aggregation: 'NONE',
realtime: true,
datapointsInitialDisplayLimit: 3,
datapoints: [
{
fragment: 'c8y_Battery',
series: 'level',
label: this.translateService.instant('Battery: level'),
unit: '%',
renderType: 'min',
lineType: 'line',
color: '#5e07b3',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'rssi',
label: this.translateService.instant('Signal: RSSI'),
unit: 'dBm',
renderType: 'min',
lineType: 'line',
color: '#a7d0f1',
__active: true
},
{
fragment: 'c8y_MemoryMeasurement',
series: 'Used',
label: this.translateService.instant('Memory: used'),
unit: 'MB',
renderType: 'min',
lineType: 'line',
color: '#f75229',
__active: true
},
{
fragment: 'c8y_CPUMeasurement',
series: 'Workload',
label: this.translateService.instant('CPU: workload'),
unit: '%',
renderType: 'min',
lineType: 'line',
color: '#b24f3e',
__active: true
},
{
fragment: 'c8y_NetworkStatistics',
series: 'Upload',
label: this.translateService.instant('Network: upload'),
unit: 'KB/s',
renderType: 'min',
lineType: 'line',
color: '#88c039',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'RCSP',
label: this.translateService.instant('Signal: RCSP'),
renderType: 'min',
lineType: 'line',
color: '#5e2f19',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'RSCP',
label: this.translateService.instant('Signal: RSCP'),
renderType: 'min',
lineType: 'line',
color: '#5e2f19',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'RSRP',
label: this.translateService.instant('Signal: RSRP'),
renderType: 'min',
lineType: 'line',
color: '#43ba3b',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'RSRQ',
label: this.translateService.instant('Signal: RSRQ'),
renderType: 'min',
lineType: 'line',
color: '#14b0b3',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'ber',
label: this.translateService.instant('Signal: BER'),
unit: '%',
renderType: 'min',
lineType: 'line',
color: '#44ff33',
__active: true
},
{
fragment: 'c8y_SignalStrength',
series: 'ECN0',
label: this.translateService.instant('Signal: ECN0'),
renderType: 'min',
lineType: 'line',
color: '#873efa',
__active: true
},
{
fragment: 'c8y_NetworkStatistics',
series: 'Download',
label: this.translateService.instant('Network: download'),
unit: 'KB/s',
renderType: 'min',
lineType: 'line',
color: '#f27a45',
__active: true
},
{
fragment: 'c8y_MemoryMeasurement',
series: 'Total',
label: this.translateService.instant('Memory: total'),
unit: 'MB',
renderType: 'min',
lineType: 'line',
color: '#dadd8c',
__active: true
}
],
alarmsEventsConfigs: [
{
label: 'c8y_LocationUpdate',
timelineType: 'EVENT',
color: '#6f6cd0',
filters: {
type: 'c8y_LocationUpdate'
},
__active: true
},
{
label: 'c8y_UnavailabilityAlarm',
timelineType: 'ALARM',
color: '#375bf2',
filters: {
type: 'c8y_UnavailabilityAlarm'
},
__active: true
}
]
}
},
{
name: 'Asset Properties',
title: gettext('Device data'),
config: {
options: {
properties: [
{
id: 'c8ySchema!!id',
__active: true,
keyPath: ['id'],
type: 'string',
label: 'ID'
},
{
id: 'c8ySchema!!name',
__active: true,
keyPath: ['name'],
type: 'string',
label: 'Name'
},
{
id: 'c8ySchema!!type',
__active: true,
keyPath: ['type'],
type: 'string',
label: 'Type'
},
{
id: 'c8ySchema!!lastUpdated',
__active: true,
keyPath: ['lastUpdated'],
type: 'string',
label: 'Last Updated'
},
{
id: 'c8ySchema!!creationTime',
__active: true,
keyPath: ['creationTime'],
type: 'string',
label: 'Creation Time'
}
],
allowDisplayAndEditKnownProperties: false,
hiddenPropertiesKeyPaths: ['owner']
}
},
_x: 0,
_y: 5,
_height: 8,
_width: 8
},
{
name: 'Alarm list',
title: gettext('Active, critical alarms'),
_x: 8,
_y: 5,
_height: 4,
_width: 8,
config: {
options: {
orderMode: 'ACTIVE_FIRST',
severity: { CRITICAL: true },
status: {
ACTIVE: true,
ACKNOWLEDGED: false,
CLEARED: false
},
types: []
}
}
},
{
name: 'deviceGroupAssignmentWidget',
title: gettext('Group assignment'),
_x: 16,
_y: 5,
_height: 4,
_width: 8
},
{
componentId: 'Map',
title: gettext('Location'),
_x: 8,
_y: 9,
_height: 4,
_width: 16
}
];
}
ngOnInit() {
this.contextSubscription = this.router.parent.data
.pipe(tap(({ contextData }) => {
const contextNameValid = contextData.name && contextData.name.trim();
this.title = contextNameValid ? contextData.name : contextData.id;
this.dashboardName = `${contextData.id}-device-info`;
}))
.subscribe(async (data) => {
this.currentContext = data;
this.addDeviceTargetForDP(data.contextData.id, data.contextData.name);
});
this.widgetsSubscription = from(this.DEVICE_INFO_WIDGETS)
.pipe(mergeMap(widget => this.widgetService.mapLegacy(widget)), map(widget => {
let device = this.router.parent.snapshot.data.contextData;
if (device) {
device = {
name: device.name,
id: device.id
};
}
return {
...widget,
config: {
...widget.config,
device
}
};
}), toArray())
.subscribe(async (data) => {
await this.filterSupportedSeriesForDP(this.currentContext.contextData.id, data);
this.widgetsList = data;
this.isLoading = false;
});
}
addDeviceTargetForDP(id, name) {
const dpWidget = this.DEVICE_INFO_WIDGETS.filter(item => item.name == 'Data points graph')[0];
// Add target to dataPoint config
dpWidget.config.datapoints.map(item => (item['__target'] = { id, name }));
// Add target to alarmEvents Config
dpWidget.config.alarmsEventsConfigs.map(item => (item['__target'] = { id, name }));
}
async filterSupportedSeriesForDP(id, widgets) {
const supportedMeasurements = await this.inventoryService.getMeasurementsAndSeries(id);
const isDataPointSupported = dp => supportedMeasurements.some(({ fragment, series }) => dp.fragment === fragment && dp.series === series);
const dpWidget = widgets.find(({ name }) => name === 'Data points graph');
dpWidget.config.datapoints = dpWidget.config.datapoints.filter(isDataPointSupported);
}
ngOnDestroy() {
this.widgetsSubscription.unsubscribe();
this.contextSubscription.unsubscribe();
}
canDeactivate() {
return this.contextDashboard?.canDeactivate();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceInfoDashboardComponent, deps: [{ token: i1.ActivatedRoute }, { token: WidgetService }, { token: i2$1.TranslateService }, { token: i1$1.InventoryService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DeviceInfoDashboardComponent, isStandalone: true, selector: "c8y-device-info-dashboard-dashboard", viewQueries: [{ propertyName: "contextDashboard", first: true, predicate: ContextDashboardComponent, descendants: true }], ngImport: i0, template: "<div *ngIf=\"!isLoading\">\n <c8y-title>\n {{ title }}\n </c8y-title>\n <c8y-context-dashboard\n [translateWidgetTitle]=\"true\"\n [name]=\"dashboardName\"\n [context]=\"currentContext\"\n [defaultWidgets]=\"widgetsList\"\n [canDelete]=\"false\"\n [hideAvailability]=\"true\"\n [canCopy]=\"false\"\n ></c8y-context-dashboard>\n</div>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "component", type: ContextDashboardComponent, selector: "c8y-context-dashboard", inputs: ["name", "childrenClasses", "context", "setTitle", "defaultWidgets", "canCopy", "canDelete", "isLoading", "breadcrumbSettings", "showContextHelpButton", "translateWidgetTitle", "hideAvailability"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceInfoDashboardComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-device-info-dashboard-dashboard', imports: [NgIf, TitleComponent, ContextDashboardComponent], template: "<div *ngIf=\"!isLoading\">\n <c8y-title>\n {{ title }}\n </c8y-title>\n <c8y-context-dashboard\n [translateWidgetTitle]=\"true\"\n [name]=\"dashboardName\"\n [context]=\"currentContext\"\n [defaultWidgets]=\"widgetsList\"\n [canDelete]=\"false\"\n [hideAvailability]=\"true\"\n [canCopy]=\"false\"\n ></c8y-context-dashboard>\n</div>\n" }]
}], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: WidgetService }, { type: i2$1.TranslateService }, { type: i1$1.InventoryService }], propDecorators: { contextDashboard: [{
type: ViewChild,
args: [ContextDashboardComponent, { static: false }]
}] } });
class DeviceInfoDashboardModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceInfoDashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DeviceInfoDashboardModule, imports: [CommonModule, CoreModule, ContextDashboardModule, DeviceInfoDashboardComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceInfoDashboardModule, providers: [
hookRoute({
context: ViewContext.Device,
path: 'device-info',
component: DeviceInfoDashboardComponent,
label: gettext('Info'),
priority: 10000,
icon: 'asterisk',
canDeactivate: [(component) => component.canDeactivate()],
rootContext: ViewContext.Dashboard
})
], imports: [CommonModule, CoreModule, ContextDashboardModule, DeviceInfoDashboardComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceInfoDashboardModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, CoreModule, ContextDashboardModule, DeviceInfoDashboardComponent],
exports: [],
providers: [
hookRoute({
context: ViewContext.Device,
path: 'device-info',
component: DeviceInfoDashboardComponent,
label: gettext('Info'),
priority: 10000,
icon: 'asterisk',
canDeactivate: [(component) => component.canDeactivate()],
rootContext: ViewContext.Dashboard
})
]
}]
}] });
class DeviceManagementHomeDashboardComponent {
constructor(widgetService) {
this.widgetService = widgetService;
this.widgets = [];
this.dashboardName = 'home-dm1';
this.defaultWidgets = [
{
name: defaultWidgetIds.DEVICE_MANAGEMENT_WELCOME,
title: gettext('Quick links'),
_x: 0,
_y: 0,
_height: 4,
_width: 12
},
{
name: 'Recent Alarms',
title: gettext('Recent alarms'),
_x: 12,
_y: 0,
_height: 4,
_width: 12
}
];
}
ngOnInit() {
this.defaultWidgets.map(async (widget) => {
await this.widgetService
.mapLegacy(widget)
.then(transformedWidget => this.widgets.push(transformedWidget));
});
}
canDeactivate() {
return this.contextDashboard?.canDeactivate();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceManagementHomeDashboardComponent, deps: [{ token: WidgetService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DeviceManagementHomeDashboardComponent, isStandalone: true, selector: "c8y-device-management-home-dashboard", viewQueries: [{ propertyName: "contextDashboard", first: true, predicate: ContextDashboardComponent, descendants: true, static: true }], ngImport: i0, template: "<c8y-title>\n {{ 'Home' | translate }}\n</c8y-title>\n\n<c8y-context-dashboard\n [name]=\"dashboardName\"\n [defaultWidgets]=\"defaultWidgets\"\n [canDelete]=\"false\"\n [hideAvailability]=\"true\"\n></c8y-context-dashboard>\n", dependencies: [{ kind: "component", type: TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "component", type: ContextDashboardComponent, selector: "c8y-context-dashboard", inputs: ["name", "childrenClasses", "context", "setTitle", "defaultWidgets", "canCopy", "canDelete", "isLoading", "breadcrumbSettings", "showContextHelpButton", "translateWidgetTitle", "hideAvailability"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceManagementHomeDashboardComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-device-management-home-dashboard', imports: [TitleComponent, ContextDashboardComponent, C8yTranslatePipe], template: "<c8y-title>\n {{ 'Home' | translate }}\n</c8y-title>\n\n<c8y-context-dashboard\n [name]=\"dashboardName\"\n [defaultWidgets]=\"defaultWidgets\"\n [canDelete]=\"false\"\n [hideAvailability]=\"true\"\n></c8y-context-dashboard>\n" }]
}], ctorParameters: () => [{ type: WidgetService }], propDecorators: { contextDashboard: [{
type: ViewChild,
args: [ContextDashboardComponent, { static: true }]
}] } });
class DeviceManagementHomeDashboardModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceManagementHomeDashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DeviceManagementHomeDashboardModule, imports: [CommonModule,
CoreModule,
ContextDashboardModule,
DeviceManagementHomeDashboardComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceManagementHomeDashboardModule, providers: [
hookRoute({
path: 'home',
component: DeviceManagementHomeDashboardComponent,
canDeactivate: [(component) => component.canDeactivate()],
rootContext: ViewContext.Dashboard
}),
hookNavigator({ label: gettext('Home'), icon: 'home', priority: 10000, path: '/home' })
], imports: [CommonModule,
CoreModule,
ContextDashboardModule,
DeviceManagementHomeDashboardComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DeviceManagementHomeDashboardModule, decorators: [{
type: NgModule,
args: [{
imports: [
CommonModule,
CoreModule,
ContextDashboardModule,
DeviceManagementHomeDashboardComponent
],
exports: [],
providers: [
hookRoute({
path: 'home',
component: DeviceManagementHomeDashboardComponent,
canDeactivate: [(component) => component.canDeactivate()],
rootContext: ViewContext.Dashboard
}),
hookNavigator({ label: gettext('Home'), icon: 'home', priority: 10000, path: '/home' })
]
}]
}] });
class WidgetConfigAppearanceComponent {
constructor() {
this.widgetConfig = inject(WidgetConfigComponent);
this.possibleStyling = { WIDGET_HEADER_CLASSES, WIDGET_CONTENT_CLASSES };
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigAppearanceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetConfigAppearanceComponent, isStandalone: true, selector: "c8y-widget-config-appearance", ngImport: i0, template: "<div class=\"row\">\n <div class=\"col-xs-6\">\n <c8y-appearance-settings\n [(themeClass)]=\"widgetConfig.styling.contentClass\"\n [(headerClass)]=\"widgetConfig.styling.headerClass\"\n [possibleStylingTheme]=\"possibleStyling.WIDGET_CONTENT_CLASSES\"\n [possibleStylingHeader]=\"possibleStyling.WIDGET_HEADER_CLASSES\"\n [defaultThemeClass]=\"widgetConfig.defaultStyling.contentClass\"\n [defaultHeaderClass]=\"widgetConfig.defaultStyling.headerClass\"\n ></c8y-appearance-settings>\n </div>\n <div class=\"col-xs-6 sticky-header-top-0\">\n <c8y-widget-preview\n style=\"height: calc(100vh - 382px)\"\n [previewClasses]=\"widgetConfig.getStyle(true)\"\n ></c8y-widget-preview>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: AppearanceSettingsComponent, selector: "c8y-appearance-settings", inputs: ["themeClass", "headerClass", "defaultThemeClass", "defaultHeaderClass", "dashboardSettings", "possibleStylingTheme", "possibleStylingHeader", "columns"], outputs: ["themeClassChange", "headerClassChange", "onChange"] }, { kind: "component", type: WidgetPreviewComponent, selector: "c8y-widget-preview", inputs: ["previewClasses"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetConfigAppearanceComponent, decorators: [{
type: Component,
args: [{ standalone: true, imports: [AppearanceSettingsComponent, NgIf, WidgetPreviewComponent], selector: 'c8y-widget-config-appearance', template: "<div class=\"row\">\n <div class=\"col-xs-6\">\n <c8y-appearance-settings\n [(themeClass)]=\"widgetConfig.styling.contentClass\"\n [(headerClass)]=\"widgetConfig.styling.headerClass\"\n [possibleStylingTheme]=\"possibleStyling.WIDGET_CONTENT_CLASSES\"\n [possibleStylingHeader]=\"possibleStyling.WIDGET_HEADER_CLASSES\"\n [defaultThemeClass]=\"widgetConfig.defaultStyling.contentClass\"\n [defaultHeaderClass]=\"widgetConfig.defaultStyling.headerClass\"\n ></c8y-appearance-settings>\n </div>\n <div class=\"col-xs-6 sticky-header-top-0\">\n <c8y-widget-preview\n style=\"height: calc(100vh - 382px)\"\n [previewClasses]=\"widgetConfig.getStyle(true)\"\n ></c8y-widget-preview>\n </div>\n</div>\n" }]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ALL_GLOBAL_ROLES_SELECTED, AddDashboardComponent, AddDashboardFactory, AppearanceSettingsComponent, CONTEXT_DASHBOARD_CONFIG, ContextDashboardComponent, ContextDashboardModule, ContextDashboardService, ContextDashboardType, DASHBOARD_CHILDREN_STATE_NAME, DASHBOARD_DETAILS_OUTLET, DASHBOARD_DETAILS_TABS_OUTLET_NAME, DASHBOARD_SETTINGS_CHANGES, DASHBOARD_THEME_CLASSES, DashboardActionBarFactory, DashboardDetailComponent, DashboardDetailService, DashboardDetailsTabId, DeviceInfoDashboardComponent, DeviceInfoDashboardModule, DeviceManagementHomeDashboardComponent, DeviceManagementHomeDashboardModule, HOOK_WIDGET_CONFIG, NewDashboardGuard, PRODUCT_EXPERIENCE, PasteDashboardActionComponent, REPORT_DEFAULT_NAVIGATION_NODE_PRIORITY, STYLING_CLASS_PREFIXES, TypeDashboardInfoComponent, WIDGET_CONTENT_CLASSES, WIDGET_HEADER_CLASSES, WidgetAssetSelectorComponent, WidgetConfigAppearanceComponent, WidgetConfigComponent, WidgetConfigFeedbackComponent, WidgetConfigGeneralComponent, WidgetConfigSectionComponent, WidgetConfigSectionService, WidgetConfigService, WidgetPreviewComponent, WidgetPreviewWrapperComponent, WidgetService, hookWidgetConfig, newDashboardTab };
//# sourceMappingURL=c8y-ngx-components-context-dashboard.mjs.map