@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
702 lines • 118 kB
JavaScript
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { InventoryService, QueriesUtil } from '@c8y/client';
import { AlertService, DynamicComponentService, getActivatedRoute, gettext, GroupService, ModalService, NavigatorNode, NavigatorService, NEW_DASHBOARD_ROUTER_STATE_PROP, OptionsService, Permissions, Status, TabsService, ViewContext } from '@c8y/ngx-components';
import { TranslateService } from '@ngx-translate/core';
import { assign, cloneDeep, forEach, get, has, keyBy, keys, pick, reduce, set, some } from 'lodash-es';
import { combineLatest, from, of, Subject } from 'rxjs';
import { catchError, filter, map, mergeMap, tap, throwIfEmpty, toArray } from 'rxjs/operators';
import { ALL_GLOBAL_ROLES_SELECTED, ContextDashboardType, PRODUCT_EXPERIENCE, STYLING_CLASS_PREFIXES } from './context-dashboard.model';
import * as i0 from "@angular/core";
import * as i1 from "@c8y/client";
import * as i2 from "@c8y/ngx-components";
import * as i3 from "@ngx-translate/core";
import * as i4 from "@angular/router";
export 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 && 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 (!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$;
}
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.getTemporaryDashboard({
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 {
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;
}
getTemporaryDashboard(dashboardCfg) {
return {
c8y_Dashboard: {
...dashboardCfg,
name: dashboardCfg.name || 'Temporary Dashboard',
isTransient: true,
description: gettext('This is a temporary, non-editable dashboard displayed due to insufficient permissions.')
},
[`c8y_Dashboard!name!${dashboardCfg.name}`]: {}
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ContextDashboardService, deps: [{ token: i1.InventoryService }, { token: i2.TabsService }, { token: i2.ModalService }, { token: i3.TranslateService }, { token: i4.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: "18.2.13", ngImport: i0, type: ContextDashboardService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ContextDashboardService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: i1.InventoryService }, { type: i2.TabsService }, { type: i2.ModalService }, { type: i3.TranslateService }, { type: i4.Router }, { type: i2.NavigatorService }, { type: i2.Permissions }, { type: i2.AlertService }, { type: i2.DynamicComponentService }, { type: i2.GroupService }, { type: i2.OptionsService }] });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGV4dC1kYXNoYm9hcmQuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL2NvbnRleHQtZGFzaGJvYXJkL2NvbnRleHQtZGFzaGJvYXJkLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUMzQyxPQUFPLEVBQTBCLE1BQU0sRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ2pFLE9BQU8sRUFBa0IsZ0JBQWdCLEVBQWUsV0FBVyxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQ3pGLE9BQU8sRUFDTCxZQUFZLEVBR1osdUJBQXVCLEVBQ3ZCLGlCQUFpQixFQUNqQixPQUFPLEVBQ1AsWUFBWSxFQUNaLFlBQVksRUFDWixhQUFhLEVBQ2IsZ0JBQWdCLEVBQ2hCLCtCQUErQixFQUMvQixjQUFjLEVBQ2QsV0FBVyxFQUNYLE1BQU0sRUFFTixXQUFXLEVBQ1gsV0FBVyxFQUVaLE1BQU0scUJBQXFCLENBQUM7QUFDN0IsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDdkQsT0FBTyxFQUNMLE1BQU0sRUFDTixTQUFTLEVBQ1QsT0FBTyxFQUNQLEdBQUcsRUFDSCxHQUFHLEVBQ0gsS0FBSyxFQUNMLElBQUksRUFDSixJQUFJLEVBQ0osTUFBTSxFQUNOLEdBQUcsRUFDSCxJQUFJLEVBQ0wsTUFBTSxXQUFXLENBQUM7QUFDbkIsT0FBTyxFQUFFLGFBQWEsRUFBRSxJQUFJLEVBQWMsRUFBRSxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUNwRSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsUUFBUSxFQUFFLEdBQUcsRUFBRSxZQUFZLEVBQUUsT0FBTyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDL0YsT0FBTyxFQUNMLHlCQUF5QixFQUl6QixvQkFBb0IsRUFHcEIsa0JBQWtCLEVBQ2xCLHNC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