UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

722 lines (715 loc) 129 kB
import * as i0 from '@angular/core'; import { Injectable, signal, inject, EventEmitter, Output, Input, Component, DestroyRef, ViewChild, ChangeDetectionStrategy } from '@angular/core'; import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string'; import * as i2 from '@c8y/ngx-components'; import { AlertService, MOChunkLoaderService, DatapointSyncService, ContextRouteService, ClipboardService, ViewContext, CoreModule, DynamicComponentAlertAggregator, Permissions, GainsightService, CommonModule, FormsModule as FormsModule$1, ResizableGridComponent, WidgetTimeContextDateRangeService, IconDirective } from '@c8y/ngx-components'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i5 from '@angular/cdk/a11y'; import { A11yModule } from '@angular/cdk/a11y'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i2$1 from '@angular/forms'; import { FormBuilder, FormsModule, ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms'; import { Router, ActivatedRoute, NavigationStart } from '@angular/router'; import { InventoryService, MeasurementService } from '@c8y/client'; import * as i4$1 from '@c8y/ngx-components/alarm-event-selector'; import { AlarmEventSelectorModule } from '@c8y/ngx-components/alarm-event-selector'; import * as i1 from '@c8y/ngx-components/context-dashboard'; import { ContextDashboardService, ContextDashboardType, ContextDashboardModule } from '@c8y/ngx-components/context-dashboard'; import * as i3$1 from '@c8y/ngx-components/datapoint-selector'; import { DatapointSelectorModule } from '@c8y/ngx-components/datapoint-selector'; import { DatapointsExportSelectorComponent } from '@c8y/ngx-components/datapoints-export-selector'; import { CHART_VIEW_CONTEXT, PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH } from '@c8y/ngx-components/echart/models'; import { ChartHelpersService, ChartsComponent, ChartEventsService, ChartAlarmsService } from '@c8y/ngx-components/echart'; import * as i4 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import * as i7 from 'ngx-bootstrap/collapse'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import * as i6 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import * as i6$1 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { INTERVALS } from '@c8y/ngx-components/interval-picker'; import { TimeContextComponent } from '@c8y/ngx-components/time-context'; import { BsModalService } from 'ngx-bootstrap/modal'; import { firstValueFrom, merge, of, fromEvent, map as map$1, startWith, filter, take } from 'rxjs'; import * as i3 from '@angular/common'; import { ReportDashboardModule, ReportDashboardService } from '@c8y/ngx-components/report-dashboard'; import { map } from 'rxjs/operators'; const adjectives = [ 'caffeinated', 'sleepy', 'hungry', 'puzzled', 'overexcited', 'daydreaming', 'chocolate-loving', 'coffee-powered', 'cookie-craving', 'disco-ready', 'weekend-mode', 'pizza-powered', 'nap-seeking', 'wifi-hunting', 'battery-hungry', 'donut-powered', 'tea-sipping', 'keyboard-loving', 'screen-staring', 'mouse-chasing', 'code-dreaming', 'pixel-perfect', 'bug-finding', 'zoom-tired', 'meeting-dodging', 'deadline-racing', 'coffee-seeking', 'sandwich-craving', 'debug-ready', 'rest-needing' ]; const nouns = [ 'sensor', 'robot', 'thermostat', 'gateway', 'dashboard', 'widget', 'gadget', 'button', 'antenna', 'beacon', 'adapter', 'gizmo', 'hub', 'switch', 'chip', 'controller', 'display', 'terminal', 'processor', 'transmitter', 'receiver', 'pod', 'device', 'module', 'relay', 'node', 'bridge', 'screen', 'router', 'box' ]; class NameGeneratorService { generateName() { const getRandomElement = (arr) => arr[Math.floor(Math.random() * arr.length)]; const randomAdjective = getRandomElement(adjectives); const randomNoun = getRandomElement(nouns); return `${randomAdjective}_${randomNoun}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NameGeneratorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NameGeneratorService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NameGeneratorService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const DATA_EXPLORER_BASE_CONFIG = { datapoints: [], alarmsEventsConfigs: [], dateFrom: null, dateTo: null, interval: 'hours', aggregation: null, realtime: false, isAutoRefreshEnabled: false, refreshInterval: 0, displayMarkedLine: true, displayMarkedPoint: true, mergeMatchingDatapoints: false, forceMergeDatapoints: false, showLabelAndUnit: true, showSlider: true, displayDateSelection: false, yAxisSplitLines: false, xAxisSplitLines: false, numberOfDecimalPlaces: 2 }; const REVERSE_KEY_MAP = { d: 'datapoints', a: 'alarmsEventsConfigs', f: 'fragment', s: 'series', t: '__target', m: '__template', l: 'label', r: 'filters', y: 'timelineType', df: 'dateFrom', dt: 'dateTo', ac: '__active', c: 'color', i: 'id', tp: 'type' }; const KEY_MAP = Object.fromEntries(Object.entries(REVERSE_KEY_MAP).map(([shortKey, longKey]) => [longKey, shortKey])); class WorkspaceConfigurationService { constructor() { this.baseKey = 'c8y-configs'; this.baseDefaultKey = 'c8y-default-config-id'; this.contextIdSignal = signal(null, ...(ngDevMode ? [{ debugName: "contextIdSignal" }] : [])); this.alertService = inject(AlertService); } get LOCAL_STORAGE_KEY() { const id = this.contextIdSignal(); return id !== null ? `${this.baseKey}-${id}` : this.baseKey; } get LOCAL_STORAGE_DEFAULT_ID_KEY() { const id = this.contextIdSignal(); return id !== null ? `${this.baseDefaultKey}-${id}` : this.baseDefaultKey; } /** * Generates a full datapoint explorer link from a bare config */ generateExplorerLink(config, label, id) { const diffed = this.removeDefaults(config); const minified = this.minifyKeys(diffed); const compressed = compressToEncodedURIComponent(JSON.stringify(minified)); const url = `/datapointexplorer-v2?id=${id}&label=${encodeURIComponent(label)}&config=${compressed}`; return url; } /** Load workspace configs from localStorage */ getConfigurations() { const configurations = localStorage.getItem(this.LOCAL_STORAGE_KEY); if (configurations) { const parsedConfigs = JSON.parse(configurations); return parsedConfigs.map(workspaceConfig => { if (typeof workspaceConfig.config === 'string') { return { ...workspaceConfig, config: this.decodeConfig(workspaceConfig.config) }; } return workspaceConfig; }); } return []; } getDefaultConfigurationId() { return localStorage.getItem(this.LOCAL_STORAGE_DEFAULT_ID_KEY); } /** Save workspace configs in localStorage */ saveConfigurations(configurations, id) { // Before saving, we need to clean up the config objects to remove any unnecessary properties in the __target object configurations = configurations.map(workspace => ({ ...workspace, config: this.cleanUpTargetObject(workspace.config) })); localStorage.setItem(this.LOCAL_STORAGE_KEY, JSON.stringify(configurations)); localStorage.setItem(this.LOCAL_STORAGE_DEFAULT_ID_KEY, id); } /** * * @param urlConfig - configuration from the URL, either compressed string or already decoded object * @returns */ getConfigurationFromUrl(urlConfig) { if (typeof urlConfig === 'string') { const decodedConfig = this.decodeConfig(urlConfig); const expandedConfig = this.expandKeys(decodedConfig); return expandedConfig; } return this.expandKeys(urlConfig); } /** * Encode a config for the URL: * - Cleanup the __target objects to contain only name and id * - Remove default values (diff from base) * - Minify keys * - Compress with lz-string */ encodeConfig(config) { const normalized = this.cleanUpTargetObject(config); const diffed = this.removeDefaults(normalized); const minified = this.minifyKeys(diffed); return compressToEncodedURIComponent(JSON.stringify(minified)); } /** * Decode a config from the URL: * - Decompress * - Expand keys * - Merge with base config */ decodeConfig(urlConfig) { if (!urlConfig) return null; try { const decompressed = decompressFromEncodedURIComponent(urlConfig); if (!decompressed) return null; const parsed = JSON.parse(decompressed); const expanded = this.expandKeys(parsed); return { ...DATA_EXPLORER_BASE_CONFIG, ...expanded }; } catch { this.alertService.danger(gettext('The decoded configuration is invalid and could not be loaded.')); } } /** Minify keys recursively using KEY_MAP */ minifyKeys(config) { if (Array.isArray(config)) return config.map(object => this.minifyKeys(object)); if (config !== Object(config)) return config; return Object.fromEntries(Object.entries(config).map(([originalKey, originalValue]) => [ KEY_MAP[originalKey] || originalKey, this.minifyKeys(originalValue) ])); } /** Expand keys recursively using REVERSE_KEY_MAP */ expandKeys(config) { if (Array.isArray(config)) return config.map(o => this.expandKeys(o)); if (config !== Object(config)) return config; return Object.fromEntries(Object.entries(config).map(([originalKey, originalValue]) => [ REVERSE_KEY_MAP[originalKey] || originalKey, this.expandKeys(originalValue) ])); } /** * Remove properties from `config` that match `base` so only changed values remain. */ removeDefaults(config) { const result = {}; for (const key in config) { if (!(key in DATA_EXPLORER_BASE_CONFIG) || DATA_EXPLORER_BASE_CONFIG[key] !== config[key]) { result[key] = config[key]; } } return result; } /** * Cleans up __target objects to only keep id and name and remove the rest. */ cleanUpTargetObject(config) { const configCopy = { ...config }; // Cleanup the datapoints array if (Array.isArray(configCopy.datapoints)) { configCopy.datapoints = configCopy.datapoints.map((dp) => { if (dp && typeof dp === 'object' && dp.__target && typeof dp.__target === 'object') { const { id, name } = dp.__target; const target = {}; if (id !== undefined) target.id = id; if (name !== undefined) target.name = name; return { ...dp, __target: target }; } return dp; }); } // Cleanup the alarmEventsConfig array if (Array.isArray(configCopy.alarmsEventsConfigs)) { configCopy.alarmsEventsConfigs = configCopy.alarmsEventsConfigs.map((a) => { if (a && typeof a === 'object' && a.__target && typeof a.__target === 'object') { const { id, name } = a.__target; const target = {}; if (id !== undefined) target.id = id; if (name !== undefined) target.name = name; return { ...a, __target: target }; } return a; }); } return configCopy; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: WorkspaceConfigurationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: WorkspaceConfigurationService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: WorkspaceConfigurationService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class DataExplorerService { constructor() { this.inventory = inject(InventoryService); this.contextDashboardService = inject(ContextDashboardService); this.workspaceConfigurationService = inject(WorkspaceConfigurationService); this.moChunkLoader = inject(MOChunkLoaderService); this.datapointSyncService = inject(DatapointSyncService); this.router = inject(Router); this.maxNumberOfManagedObjectsPerRequest = 50; } async fetchReportDashboard(reportId) { return (await this.inventory.list({ pageSize: 1, query: `has('c8y_Dashboard!name!report_${reportId}')` })).data[0]; } async fetchContextDashboard(dashboardId, contextAsset) { const context = contextAsset.c8y_isDevice ? ContextDashboardType.Device : ContextDashboardType.Group; return firstValueFrom(this.contextDashboardService.getDashboard$(dashboardId, [context])); } async loadManagedObjectsInChunks(uniqIds) { const { results, errors } = await this.moChunkLoader.processInChunks(uniqIds, this.maxNumberOfManagedObjectsPerRequest, ids => this.loadAChunkOfManagedObjects(ids)); return { result: results, errors }; } /** * Navigate to datapoint explorer with given config. * The goal of this method is to navigate to the data explorer with a provided config from any other application. * @param config Configuration to use * @param label Label to be displayed for the configuration * @param id ID for the configuration */ navigateToDataExplorer(config, label, id) { const url = this.getUrlForConfig(config, label, id); this.router.navigateByUrl(url); } /** * Generate a URL for the datapoint explorer with the given config. * The goal of this method is to generate a shareable link to the data explorer. * @param config Configuration to use * @param label Label to be displayed for the configuration * @param id ID for the configuration * @returns The generated URL */ getUrlForConfig(config, label, id) { return this.workspaceConfigurationService.generateExplorerLink(config, label, id); } processAlarmEventConfigs(config) { const firstTarget = config.alarmsEventsConfigs.find(ae => ae.__target)?.__target; config.alarmsEventsConfigs = config.alarmsEventsConfigs.map((ae, index) => { if (ae.__active === undefined) ae.__active = true; if (!ae.__target && firstTarget) ae.__target = firstTarget; if (!ae.color) ae.color = this.generateColor(index); return ae; }); } processDatapoints(config) { const firstTarget = config.datapoints.find(dp => dp.__target)?.__target; config.datapoints = config.datapoints.map((dp, index) => { // Default __active if (dp.__active === undefined) dp.__active = true; if (!dp.__target && firstTarget) dp.__target = firstTarget; if (!dp.color && !dp.__template) dp.color = this.generateColor(index); if (!dp.label) dp.label = `${dp.fragment} -> ${dp.series}`; return dp; }); } /** * Generates a color from a fixed palette based on the index. * Used to assign colors to alarm/event configs in the UI. */ generateColor(index) { // Simple palette, can expand const palette = ['#c87d33', '#8c145f', '#8cd7fd', '#59a036', '#fb00ff', '#8d4c22', '#fbb2d7']; return palette[index % palette.length]; } async loadAndAssignManagedObjects(config, uniqueIds) { const managedObjectsResult = await this.loadManagedObjectsInChunks([...uniqueIds]); const managedObjects = managedObjectsResult.result; const errors = managedObjectsResult.errors; config.datapoints = this.datapointSyncService.assignUpdatedValues(config.datapoints, managedObjects, errors); } async loadAChunkOfManagedObjects(uniqIds) { return this.moChunkLoader.loadAChunkOfManagedObjectsBase(uniqIds, this.inventory, this.maxNumberOfManagedObjectsPerRequest, id => this.moChunkLoader.getStatusDetails(id)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DataExplorerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DataExplorerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DataExplorerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class WorkspaceConfigComponent { constructor() { this.silent = false; this.onConfigurationChange = new EventEmitter(); this.configurations = []; this.navigationSubscription = null; this.shouldCleanParams = false; this.activeConfigTooltip = gettext('Active configuration cannot be removed'); this.removeConfigTooltip = gettext('Remove configuration'); this.formBuilder = inject(FormBuilder); this.workspaceConfigurationService = inject(WorkspaceConfigurationService); this.contextRouteService = inject(ContextRouteService); this.clipboardService = inject(ClipboardService); this.activatedRoute = inject(ActivatedRoute); this.router = inject(Router); this.nameGeneratorService = inject(NameGeneratorService); this.datapointSyncService = inject(DatapointSyncService); this.dataExplorerService = inject(DataExplorerService); } ngOnInit() { this.initializeContextSourceId(); this.initializeConfigurations(); this.initWorkspaceForm(); this.subscribeToRouterEvents(); } ngOnChanges(changes) { if (changes.updatedConfig && !changes.updatedConfig.firstChange) { this.updateConfigurations(); } } ngOnDestroy() { if (this.shouldCleanParams) { this.router.navigate([], { queryParams: { config: null, id: null, label: null }, queryParamsHandling: 'merge' }); } this.navigationSubscription?.unsubscribe(); } addConfig(duplicatedConfig, id, label) { const name = this.nameGeneratorService.generateName(); const workspace = { id: id || new Date().toISOString(), label: label || name, config: { datapoints: [], alarmsEventsConfigs: [] } }; if (duplicatedConfig) { workspace.config = duplicatedConfig; } this.configurations = [workspace, ...this.configurations]; this.initWorkspaceForm(); this.changeConfiguration(true, workspace); } changeConfiguration(selected, configuration) { if (!selected) { return; } if (this.currentConfiguration?.id === configuration.id) { return; } this.currentConfiguration = configuration; localStorage.setItem(this.workspaceConfigurationService.LOCAL_STORAGE_DEFAULT_ID_KEY, this.currentConfiguration.id); this.onConfigurationChange.emit(configuration.config); } updateConfigurationLabel(configuration) { this.configurations = this.configurations.map(c => c.id === configuration.id ? configuration : c); if (this.currentConfiguration.id === configuration.id) { this.currentConfiguration = configuration; } this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration?.id || ''); } deleteConfiguration(configuration) { this.configurations = this.configurations.filter(c => c.id !== configuration.id); this.initWorkspaceForm(); this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration?.id || ''); } clearAll() { this.configurations = [this.currentConfiguration]; this.initWorkspaceForm(); this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration?.id || ''); } async shareConfig(configuration) { await this.clipboardService.writeText(JSON.stringify(configuration.config)); } async addConfigFromUrl(queryParams) { const id = queryParams.id || new Date().toISOString(); const label = queryParams.label || this.nameGeneratorService.generateName(); const config = this.workspaceConfigurationService.getConfigurationFromUrl(queryParams.config || {}); this.addConfig(config, id, label); if (config.datapoints?.length) { this.dataExplorerService.processDatapoints(config); } const uniqueIds = this.datapointSyncService.getManagedObjectIds(config.datapoints || []); if (uniqueIds.length) { await this.dataExplorerService.loadAndAssignManagedObjects(config, uniqueIds); } if (config.alarmsEventsConfigs?.length) { this.dataExplorerService.processAlarmEventConfigs(config); } this.onConfigurationChange.emit(config); } updateConfigurations() { const config = { ...this.updatedConfig }; this.currentConfiguration.config = config; this.configurations = this.configurations.map(currentConfig => currentConfig.id === this.currentConfiguration.id ? this.currentConfiguration : currentConfig); const queryParams = { id: this.currentConfiguration.id, label: this.currentConfiguration.label, config: this.workspaceConfigurationService.encodeConfig(config) }; this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration.id); const control = this.configurationsFormGroup.controls['configurations']; const index = this.configurations.findIndex(c => c.id === this.currentConfiguration.id); if (index !== -1) { control.at(index).patchValue({ label: this.currentConfiguration.label, config: this.currentConfiguration.config, id: this.currentConfiguration.id }); } this.router.navigate([], { queryParams, queryParamsHandling: 'merge' }); } initializeConfigurations() { const configurations = this.workspaceConfigurationService.getConfigurations(); const defaultId = this.defaultConfigurationId ?? this.workspaceConfigurationService.getDefaultConfigurationId(); const queryParams = this.router.parseUrl(this.router.url).queryParams; if (configurations.length) { this.configurations = configurations; this.currentConfiguration = this.configurations.find(c => c.id === defaultId) || this.configurations[0]; this.onConfigurationChange.emit(this.currentConfiguration.config); if (queryParams?.id && !this.configurations.find(c => c.id === queryParams.id)) { this.addConfigFromUrl(queryParams); } else if (queryParams?.id && this.configurations.find(c => c.id === queryParams.id) && this.currentConfiguration.id !== queryParams.id) { this.currentConfiguration = this.configurations.find(c => c.id === queryParams.id); this.changeConfiguration(true, this.currentConfiguration); } } if (!this.currentConfiguration) { if (Object.keys(queryParams).length === 0) { this.addConfig(); return; } this.addConfigFromUrl(queryParams); } } initWorkspaceForm() { this.configurationsFormGroup = this.formBuilder.group({ configurations: this.formBuilder.array([]) }); this.patchForm(); } patchForm() { const control = this.configurationsFormGroup.controls['configurations']; this.configurations.forEach(workspace => { control.push(this.patchValues(workspace)); }); } patchValues(workspace) { return this.formBuilder.group({ label: [workspace.label], config: [workspace.config], id: [workspace.id] }); } /** * Subscribe to router events to clean query params when navigating away, but only when navigating between group or device context * Otherwise it breaks GC. */ subscribeToRouterEvents() { this.navigationSubscription = this.router.events.subscribe(event => { if (event instanceof NavigationStart) { const urlTree = this.router.parseUrl(event.url); if (urlTree.queryParams['config'] || urlTree.queryParams['id'] || urlTree.queryParams['label']) { this.shouldCleanParams = true; } else { this.shouldCleanParams = false; } } }); } initializeContextSourceId() { const routeContext = this.contextRouteService.getContextData(this.activatedRoute); if (!routeContext) { this.workspaceConfigurationService.contextIdSignal.set(null); return; } const { context, contextData } = routeContext; if ([ViewContext.Device, ViewContext.Group].includes(context)) { this.workspaceConfigurationService.contextIdSignal.set(contextData?.id); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: WorkspaceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: WorkspaceConfigComponent, isStandalone: true, selector: "c8y-workspace-config", inputs: { updatedConfig: "updatedConfig", silent: "silent", defaultConfigurationId: "defaultConfigurationId" }, outputs: { onConfigurationChange: "onConfigurationChange" }, usesOnChanges: true, ngImport: i0, template: "@if (!silent) {\n <div\n class=\"dropdown\"\n #actionbar_dropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"actionbar_dropdown.isOpen\"\n dropdown\n [insideClick]=\"true\"\n >\n <button\n class=\"dropdown-toggle form-control l-h-tight d-flex a-i-center\"\n attr.aria-label=\"{{ currentConfiguration?.label }}\"\n tooltip=\"{{ 'Selected configuration' | translate }}\"\n placement=\"top\"\n container=\"body\"\n data-cy=\"current-configuration-dropdown-button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"list\"\n ></i>\n <div class=\"d-col text-left fit-w\">\n <span class=\"text-12\">\n {{ 'Configuration' | translate }}\n </span>\n <span class=\"text-10 text-muted text-truncate\">\n {{ currentConfiguration?.label }}\n </span>\n </div>\n <span class=\"caret m-r-16 m-l-4\"></span>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-wide dropdown-menu-action-bar\"\n *dropdownMenu\n >\n <div class=\"sticky-top separator-bottom p-t-8 p-b-8 p-l-16 p-r-16\">\n <p>\n <strong>{{ 'Data explorer configurations' | translate }}</strong>\n </p>\n <p>\n <small>{{ 'Easily switch and manage configurations.' | translate }}</small>\n </p>\n </div>\n <c8y-list-group class=\"no-border-last\">\n <form [formGroup]=\"configurationsFormGroup\">\n <div formArrayName=\"configurations\">\n @for (\n configuration of configurationsFormGroup.controls.configurations['controls'];\n track $index\n ) {\n <c8y-li\n class=\"p-0\"\n [dense]=\"true\"\n >\n <c8y-li-radio\n style=\"min-height: 48px\"\n [selected]=\"configuration.value.id === currentConfiguration.id\"\n (onSelect)=\"changeConfiguration($event, configuration.value)\"\n ></c8y-li-radio>\n\n <div class=\"d-flex a-i-center gap-8\">\n <div\n class=\"min-width-0\"\n [formGroupName]=\"$index\"\n >\n <label\n class=\"editable\"\n [ngClass]=\"{\n updated:\n configuration.controls.label.touched && configuration.controls.label.dirty\n }\"\n >\n <input\n class=\"form-control\"\n [style.width.ch]=\"configuration.value.label || 25\"\n [attr.aria-label]=\"'Configuration label' | translate\"\n placeholder=\"{{ 'Configuration 1' | translate }}\"\n type=\"text\"\n autocomplete=\"off\"\n required\n formControlName=\"label\"\n (blur)=\"updateConfigurationLabel(configuration.value)\"\n (keydown.enter)=\"\n $event.preventDefault(); updateConfigurationLabel(configuration.value)\n \"\n />\n </label>\n </div>\n\n <div class=\"flex-nogrow d-flex gap-8\">\n <button\n class=\"btn-dot btn m-0\"\n [attr.aria-label]=\"'Duplicate configuration' | translate\"\n tooltip=\"{{ 'Duplicate configuration' | translate }}\"\n placement=\"left\"\n (click)=\"addConfig(configuration.value.config)\"\n [delay]=\"500\"\n >\n <i c8yIcon=\"copy\"></i>\n </button>\n\n <button\n class=\"btn-dot btn btn-dot--danger\"\n [attr.aria-label]=\"'Remove configurations' | translate\"\n tooltip=\"{{\n (configuration.value.id === currentConfiguration.id\n ? activeConfigTooltip\n : removeConfigTooltip\n ) | translate\n }}\"\n placement=\"left\"\n [delay]=\"500\"\n (click)=\"$event.stopPropagation(); deleteConfiguration(configuration.value)\"\n [disabled]=\"configuration.value.id === currentConfiguration.id\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n </div>\n </div>\n </c8y-li>\n }\n </div>\n </form>\n </c8y-list-group>\n <div class=\"sticky-bottom separator-top\">\n <div class=\"d-flex p-l-16 p-r-16 p-t-8 p-b-8\">\n <button\n class=\"btn btn-danger btn-sm flex-grow m-r-4\"\n (click)=\"clearAll()\"\n [disabled]=\"configurations.length < 2\"\n >\n <i [c8yIcon]=\"'delete'\"></i>\n {{ 'Delete all configurations' | translate }}\n </button>\n <button\n class=\"btn btn-default btn-sm flex-grow\"\n type=\"button\"\n (click)=\"addConfig()\"\n >\n <i [c8yIcon]=\"'add-circle-outline'\"></i>\n {{ 'Add configuration' | translate }}\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2$1.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: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "component", type: i2.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i2.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i2.ListItemRadioComponent, selector: "c8y-list-item-radio, c8y-li-radio", inputs: ["selected", "name", "disabled", "value"], outputs: ["onSelect"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i4.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i4.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i4.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i5.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i6.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: WorkspaceConfigComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-workspace-config', standalone: true, imports: [CoreModule, BsDropdownModule, FormsModule, A11yModule, TooltipModule], template: "@if (!silent) {\n <div\n class=\"dropdown\"\n #actionbar_dropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"actionbar_dropdown.isOpen\"\n dropdown\n [insideClick]=\"true\"\n >\n <button\n class=\"dropdown-toggle form-control l-h-tight d-flex a-i-center\"\n attr.aria-label=\"{{ currentConfiguration?.label }}\"\n tooltip=\"{{ 'Selected configuration' | translate }}\"\n placement=\"top\"\n container=\"body\"\n data-cy=\"current-configuration-dropdown-button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"list\"\n ></i>\n <div class=\"d-col text-left fit-w\">\n <span class=\"text-12\">\n {{ 'Configuration' | translate }}\n </span>\n <span class=\"text-10 text-muted text-truncate\">\n {{ currentConfiguration?.label }}\n </span>\n </div>\n <span class=\"caret m-r-16 m-l-4\"></span>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-wide dropdown-menu-action-bar\"\n *dropdownMenu\n >\n <div class=\"sticky-top separator-bottom p-t-8 p-b-8 p-l-16 p-r-16\">\n <p>\n <strong>{{ 'Data explorer configurations' | translate }}</strong>\n </p>\n <p>\n <small>{{ 'Easily switch and manage configurations.' | translate }}</small>\n </p>\n </div>\n <c8y-list-group class=\"no-border-last\">\n <form [formGroup]=\"configurationsFormGroup\">\n <div formArrayName=\"configurations\">\n @for (\n configuration of configurationsFormGroup.controls.configurations['controls'];\n track $index\n ) {\n <c8y-li\n class=\"p-0\"\n [dense]=\"true\"\n >\n <c8y-li-radio\n style=\"min-height: 48px\"\n [selected]=\"configuration.value.id === currentConfiguration.id\"\n (onSelect)=\"changeConfiguration($event, configuration.value)\"\n ></c8y-li-radio>\n\n <div class=\"d-flex a-i-center gap-8\">\n <div\n class=\"min-width-0\"\n [formGroupName]=\"$index\"\n >\n <label\n class=\"editable\"\n [ngClass]=\"{\n updated:\n configuration.controls.label.touched && configuration.controls.label.dirty\n }\"\n >\n <input\n class=\"form-control\"\n [style.width.ch]=\"configuration.value.label || 25\"\n [attr.aria-label]=\"'Configuration label' | translate\"\n placeholder=\"{{ 'Configuration 1' | translate }}\"\n type=\"text\"\n autocomplete=\"off\"\n required\n formControlName=\"label\"\n (blur)=\"updateConfigurationLabel(configuration.value)\"\n (keydown.enter)=\"\n $event.preventDefault(); updateConfigurationLabel(configuration.value)\n \"\n />\n </label>\n </div>\n\n <div class=\"flex-nogrow d-flex gap-8\">\n <button\n class=\"btn-dot btn m-0\"\n [attr.aria-label]=\"'Duplicate configuration' | translate\"\n tooltip=\"{{ 'Duplicate configuration' | translate }}\"\n placement=\"left\"\n (click)=\"addConfig(configuration.value.config)\"\n [delay]=\"500\"\n >\n <i c8yIcon=\"copy\"></i>\n </button>\n\n <button\n class=\"btn-dot btn btn-dot--danger\"\n [attr.aria-label]=\"'Remove configurations' | translate\"\n tooltip=\"{{\n (configuration.value.id === currentConfiguration.id\n ? activeConfigTooltip\n : removeConfigTooltip\n ) | translate\n }}\"\n placement=\"left\"\n [delay]=\"500\"\n (click)=\"$event.stopPropagation(); deleteConfiguration(configuration.value)\"\n [disabled]=\"configuration.value.id === currentConfiguration.id\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n </div>\n </div>\n </c8y-li>\n }\n </div>\n </form>\n </c8y-list-group>\n <div class=\"sticky-bottom separator-top\">\n <div class=\"d-flex p-l-16 p-r-16 p-t-8 p-b-8\">\n <button\n class=\"btn btn-danger btn-sm flex-grow m-r-4\"\n (click)=\"clearAll()\"\n [disabled]=\"configurations.length < 2\"\n >\n <i [c8yIcon]=\"'delete'\"></i>\n {{ 'Delete all configurations' | translate }}\n </button>\n <button\n class=\"btn btn-default btn-sm flex-grow\"\n type=\"button\"\n (click)=\"addConfig()\"\n >\n <i [c8yIcon]=\"'add-circle-outline'\"></i>\n {{ 'Add configuration' | translate }}\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n" }] }], propDecorators: { updatedConfig: [{ type: Input }], silent: [{ type: Input }], defaultConfigurationId: [{ type: Input }], onConfigurationChange: [{ type: Output }] } }); class CreateNewReportModalComponent { constructor(contextDashboardService) { this.contextDashboardService = contextDashboardService; this.reportName = ''; this.labels = { cancel: gettext('Cancel'), ok: gettext('Send') }; this.result = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); this.styling = { themeClass: 'dashboard-theme-light', headerClass: 'panel-title-regular' }; this.DEFAULT_DASHBOARD_ICON = 'th'; this.DEFAULT_DASHBOARD_PRIORITY = 5000; this.DEFAULT_DASHBOARD_MARGIN = 12; } async save() { const dashboard = { name: this.reportName, icon: this.DEFAULT_DASHBOARD_ICON, c8y_IsNavigatorNode: null, priority: this.DEFAULT_DASHBOARD_PRIORITY, description: '', widgetMargin: this.DEFAULT_DASHBOARD_MARGIN, classes: { [this.styling.headerClass]: true }, widgetClasses: { [this.styling.headerClass]: true }, translateDashboardTitle: true }; try { const { name, icon, c8y_IsNavigatorNode, priority, description, translateDashboardTitle } = dashboard; const report = (await this.contextDashboardService.createReport({ name, icon, c8y_IsNavigatorNode, priority, description, translateDashboardTitle })).data; await this.contextDashboardService.create(dashboard, undefined, `${this.contextDashboardService.REPORT_PARTIAL_NAME}${report.id}`); this._resolve(report); } catch (ex) { this._reject(ex); } } cancel() { this._reject(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CreateNewReportModalComponent, deps: [{ token: i1.ContextDashboardService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: CreateNewReportModalComponent, isStandalone: true, selector: "c8y-create-new-report-modal", ngImport: i0, template: "<c8y-modal\n [title]=\"'Create new report with widget' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [disabled]=\"reportName === ''\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"labels\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"c8y-reports\"></span>\n </ng-container>\n\n <p class=\"text-center bg-component text-balance sticky-top p-l-24 p-r-24 p-t-8 p-b-8 separator-bottom\">\n {{' Create a new report with the Data points graph widget using the current configuration.' | translate}}\n </p>\n <div class=\"p-24 p-t-8\">\n <c8y-form-group>\n <label\n for=\"reportName\"\n translate\n >\n Report name\n </label>\n <input\n class=\"form-control\"\n id=\"reportName\"\n placeholder=\"{{ 'e.g. My data point Report' }}\"\n name=\"name\"\n type=\"text\"\n autocomplete=\"off\"\n required\n [(ngModel)]=\"reportName\"\n />\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </div>\n</c8y-modal>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2$1.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: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: i2.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i2.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: i2.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage", "additionalMessages"] }, { kind: "directive", type: i2.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "ngmodule", type: ReportDashboardModule }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CreateNewReportModalComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-create-new-report-modal', standalone: true, imports: [ReactiveFormsModule, CoreModule, ReportDashboardModule], template: "<c8y-modal\n [title]=\"'Create new report with widget' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [disabled]=\"reportName === ''\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"labels\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"c8y-reports\"></span>\n </ng-container>\n\n <p class=\"text-center bg-component text-balance sticky-top p-l-24 p-r-24 p-t-8 p-b-8 separator-bottom\">\n {{' Create a new report with the Data points graph widget using the current configuration.' | translate}}\n </p>\n <div class=\"p-24 p-t-8\">\n <c8y-form-group>\n <label\n for=\"reportName\"\n translate\n >\n Report name\n </label>\n <input\n