UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

848 lines (841 loc) 328 kB
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