@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
848 lines (841 loc) • 328 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Component, inject, InjectionToken, EventEmitter, Input, Output, forwardRef, Optional, Inject, ViewChild, HostBinding, HostListener, TemplateRef, NgModule } from '@angular/core';
import * as i2 from '@c8y/ngx-components';
import { gettext, Permissions, ContextRouteService, ViewContext, Status, NavigatorNode, getActivatedRoute, NEW_DASHBOARD_ROUTER_STATE_PROP, SupportedApps, memoize, DashboardChildChange, CopyDashboardDisabledReason, WidgetsDashboardComponent, hookRoute, CoreModule, hookTab, HookProviderTypes, hookActionBar, hookNavigator, BaseColumn, getBasicInputArrayFormFieldConfig, DataGridService, alertOnError, BuiltInActionType, ModalModule } from '@c8y/ngx-components';
import * as i1 from '@angular/router';
import { of, Subject, from, combineLatest, iif, timer } from 'rxjs';
import * as i4 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i3 from '@angular/forms';
import { Validators, NG_VALUE_ACCESSOR } from '@angular/forms';
import * as i4$1 from 'ngx-bootstrap/popover';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { __decorate, __metadata } from 'tslib';
import * as i2$1 from '@ngx-translate/core';
import { assign, pick, cloneDeep, some, keys, keyBy, has, set, reduce, forEach, get, isEqual, clone, omit, sortBy, escapeRegExp, findIndex, kebabCase, every } from 'lodash-es';
import * as i2$2 from 'ngx-bootstrap/modal';
import * as i1$1 from '@c8y/client';
import { QueriesUtil } from '@c8y/client';
import { tap, map, catchError, throwIfEmpty, filter, mergeMap, toArray, first, switchMap } from 'rxjs/operators';
import * as i9 from 'ngx-bootstrap/collapse';
import { CollapseModule } from 'ngx-bootstrap/collapse';
import * as i5 from '@c8y/ngx-components/icon-selector';
import { IconSelectorModule } from '@c8y/ngx-components/icon-selector';
import * as i8 from '@c8y/ngx-components/assets-navigator';
import { AssetSelectorModule } from '@c8y/ngx-components/assets-navigator';
import * as i1$2 from 'ngx-bootstrap/dropdown';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { NameDeviceGridColumn, TypeDeviceGridColumn, GroupDeviceGridColumn, RegistrationDateDeviceGridColumn, DeviceGridModule } from '@c8y/ngx-components/device-grid';
const newDashboardTab = {
featureId: 'newDashboard',
icon: 'th',
label: gettext('New dashboard'),
path: 'dashboard/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: "18.2.13", ngImport: i0, type: NewDashboardGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NewDashboardGuard, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", 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: "18.2.13", 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: "18.2.13", type: AddDashboardComponent, 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: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddDashboardComponent, decorators: [{
type: Component,
args: [{ selector: '[c8y-add-dashboard]', host: { class: 'd-flex a-i-stretch sticky-right' }, 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
]);
}
}
class AddDeviceDashboardFactory extends AddDashboardFactory {
constructor() {
super(...arguments);
this.targetContext = ViewContext.Device;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddDeviceDashboardFactory, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddDeviceDashboardFactory, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddDeviceDashboardFactory, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class AddGroupDashboardFactory extends AddDashboardFactory {
constructor() {
super(...arguments);
this.targetContext = ViewContext.Group;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddGroupDashboardFactory, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddGroupDashboardFactory, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddGroupDashboardFactory, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
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')
};
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('Transparent`theme`'),
// class: 'dashboard-theme-transparent',
// description: gettext(
// 'The dashboard is styled with a transparent background and therefore is border-less.'
// )
// },
{
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'
},
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'
},
CONTEXT: {
REPORT: 'report',
DEVICE: 'device',
ASSET: 'asset',
GROUP: 'group'
},
ACTIONS: {
APPLY_GLOBAL_ROLES_CHANGES: 'applyGlobalRolesChanges',
DELETE: 'delete',
LOAD: 'load',
CREATE: 'create',
ADD_REPORT: 'addReport'
}
}
};
const REPORT_DEFAULT_NAVIGATION_NODE_PRIORITY = 30;
class AppearanceSettingsComponent {
constructor() {
this.themeClass = 'dashboard-theme-light';
this.headerClass = 'panel-title-regular';
this.themeClassChange = new EventEmitter();
this.headerClassChange = 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);
}
headerClassClick(value) {
value = this.resetToDefault(value, this.headerClass, this.defaultHeaderClass);
this.headerClass = value;
this.headerClassChange.emit(value);
}
resetToDefault(value, compareTo, defaultValue) {
if (defaultValue && value === compareTo) {
value = defaultValue;
}
return value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppearanceSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: AppearanceSettingsComponent, 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" }, ngImport: i0, template: "<div class=\"row\">\n <div class=\"col-md-{{ 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-md-{{ 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: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.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: i3.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4$1.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppearanceSettingsComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-appearance-settings', template: "<div class=\"row\">\n <div class=\"col-md-{{ 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-md-{{ 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
}], possibleStylingTheme: [{
type: Input
}], possibleStylingHeader: [{
type: Input
}], columns: [{
type: Input
}] } });
/**
* A hook to define the default widgets of the home dashboard in the cockpit app.
*/
const COCKPIT_HOME_DASHBOARD_CONFIG = new InjectionToken('CockpitHomeDashboardConfig');
/**
* An array of widgets that is per default placed on the home dashboard of the cockpit app.
*/
const DEFAULT_COCKPIT_HOME_WIDGETS = [
{
name: 'cockpit.welcome.widget',
title: gettext('Welcome to Cockpit'),
_x: 0,
_y: 0,
_height: 5,
_width: 24,
classes: { 'panel-title-hidden': true }
},
{
name: 'Asset Alarms',
title: gettext('Active, critical alarms'),
_x: 0,
_y: 5,
_height: 4,
_width: 12
},
{
name: 'Recent Alarms',
title: gettext('Recent alarms'),
_x: 0,
_y: 9,
_height: 4,
_width: 12
},
{
name: 'Map',
title: gettext('Map'),
_x: 12,
_y: 5,
_height: 8,
_width: 12
}
];
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_Dashboa