UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,157 lines (1,145 loc) 2.32 MB
import * as i0 from '@angular/core'; import { Directive, Input, Injector, InjectionToken, Injectable, Optional, Inject, isDevMode, inject, Pipe, EventEmitter, NgModule, LOCALE_ID, EnvironmentInjector, HostListener, NgModuleRef, createNgModule, Component, Output, HostBinding, forwardRef, SecurityContext, TemplateRef, APP_INITIALIZER, Self, SkipSelf, ViewChild, Attribute, ContentChild, ViewContainerRef, ContentChildren, ElementRef, ViewChildren, createComponent, runInInjectionContext, importProvidersFrom, ChangeDetectionStrategy, SimpleChange, reflectComponentType, signal, effect, Type, DestroyRef } from '@angular/core'; import * as i1$4 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule, BsDropdownDirective } from 'ngx-bootstrap/dropdown'; import * as i3 from '@angular/cdk/a11y'; import { A11yModule, CdkTrapFocus } from '@angular/cdk/a11y'; import { castArray, flatten, uniq, sortBy, groupBy, camelCase, isEqual, isUndefined, throttle as throttle$1, keys, get, isNaN as isNaN$1, isFinite, each, mapValues, mapKeys, forEach, reduce, union, cloneDeep, uniqBy, assign, min, every, first, map as map$2, find, negate, upperFirst, memoize as memoize$1, property, some, entries, omitBy, isDate, orderBy, flatMap, isEmpty, filter as filter$2, snakeCase, matches, isString, clone, toNumber, isEqualWith, escape as escape$1, escapeRegExp, assignWith, set, omit, pick, has, transform, identity, flow, isNil, chunk, values, without, indexOf, parseInt as parseInt$1, kebabCase, forOwn } from 'lodash-es'; import { merge, of, defer, combineLatest, race, isObservable, from, Subject, BehaviorSubject, NEVER, Observable, firstValueFrom, map as map$1, distinctUntilChanged as distinctUntilChanged$1, fromEvent, pipe, throwError, concat, filter as filter$1, tap as tap$1, EMPTY, timer, fromEventPattern, startWith as startWith$1, switchMap as switchMap$1, takeUntil as takeUntil$1, empty, forkJoin, ReplaySubject, interval, shareReplay as shareReplay$1, mergeMap as mergeMap$1 } from 'rxjs'; import { map, distinctUntilChanged, filter, startWith, switchMap, take, shareReplay, scan, debounceTime, share, takeUntil, tap, catchError, first as first$1, retryWhen, delay, concatMap, debounce, sample, withLatestFrom, mergeMap, every as every$1, toArray, merge as merge$1, expand, skip, mapTo, finalize, reduce as reduce$1, combineLatestWith } from 'rxjs/operators'; import * as i1 from '@c8y/client'; import { ApplicationAvailability, OperationStatus, TenantLoginOptionType, UserManagementSource, GrantType, ApplicationType, BearerAuthFromSessionStorage, FetchClient, Client, PasswordStrength, QueriesUtil, InventoryService, UserService, ApplicationService, TenantService, AlarmService, aggregationType, Service } from '@c8y/client'; import { __decorate, __metadata } from 'tslib'; import * as i1$5 from '@angular/router'; import { NavigationEnd, RouterModule as RouterModule$1, ActivationEnd, NavigationStart, PRIMARY_OUTLET, ActivatedRoute, ActivationStart, ChildActivationEnd, ROUTES, Router, NavigationCancel, NavigationError } from '@angular/router'; import * as i4 from '@c8y/ngx-components/api'; import { DataModule } from '@c8y/ngx-components/api'; import { coerceNumberProperty } from '@angular/cdk/coercion'; import * as i2$1 from '@angular/cdk/scrolling'; import { VIRTUAL_SCROLL_STRATEGY, ScrollingModule } from '@angular/cdk/scrolling'; import * as i2 from '@angular/common'; import { formatDate, registerLocaleData, DOCUMENT, DatePipe as DatePipe$1, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, NgIf, NgClass, NgTemplateOutlet, DecimalPipe, NgStyle, CommonModule as CommonModule$1, NgFor, AsyncPipe, NgSwitch, NgSwitchCase } from '@angular/common'; import * as i2$2 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { setTheme } from 'ngx-bootstrap/utils'; import * as i1$1 from '@ngx-translate/core'; import { TranslateDefaultParser, TranslateDirective, TranslatePipe, TranslateModule, TranslateLoader, TranslateCompiler, TranslateFakeCompiler, TranslateParser, MissingTranslationHandler, TranslateStore, TranslateService as TranslateService$1, USE_STORE, USE_DEFAULT_LANG, USE_EXTEND, DEFAULT_LANGUAGE } from '@ngx-translate/core'; import { gettext as gettext$1 } from '@c8y/ngx-components/gettext'; export * from '@c8y/ngx-components/gettext'; import * as i1$2 from 'ngx-bootstrap/datepicker'; import { BsDatepickerModule, BsDatepickerDirective } from 'ngx-bootstrap/datepicker'; import { defineLocale, enGbLocale, zhCnLocale, ptBrLocale, plLocale, nlLocale, koLocale, jaLocale, frLocale, esLocale, deLocale } from 'ngx-bootstrap/chronos'; import { compare, coerce } from 'semver'; import saveAs$1, { saveAs } from 'file-saver'; import * as i1$3 from '@angular/platform-browser'; import { BlobReader, ZipReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'; import * as i1$8 from 'ngx-bootstrap/collapse'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import * as i1$6 from 'ngx-bootstrap/modal'; import { ModalModule as ModalModule$1, BsModalRef } from 'ngx-bootstrap/modal'; import { sortBy as sortBy$1, isEmpty as isEmpty$1, findIndex, assign as assign$1, flatten as flatten$1, get as get$1, set as set$1, pick as pick$1 } from 'lodash'; import * as i2$3 from '@angular/forms'; import { NgModel, FormControlName, Validators, NG_VALIDATORS, ControlContainer, NgForm, NG_VALUE_ACCESSOR, FormControl, FormsModule as FormsModule$1, ReactiveFormsModule, CheckboxControlValueAccessor, FormArray, NgControl, FormGroup, FormBuilder } from '@angular/forms'; import { parsePhoneNumberFromString } from 'libphonenumber-js/max'; import * as i5 from 'angularx-qrcode'; import { QRCodeModule } from 'angularx-qrcode'; import * as i2$4 from 'ngx-bootstrap/popover'; import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover'; import * as i2$5 from '@angular/cdk/stepper'; import { CdkStepper, STEP_STATE, CdkStepperModule, CdkStep, STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper'; import * as i1$7 from '@angular/cdk/bidi'; import { getThemePreference, setThemePreference, applyTheme } from '@c8y/bootstrap'; import * as mimeDB from 'mime-db'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i6 from '@ngx-formly/core'; import { FormlyModule, FieldArrayType, FieldType, FieldWrapper, FORMLY_CONFIG, ɵdefineHiddenProp as _defineHiddenProp } from '@ngx-formly/core'; import * as i5$1 from '@ngx-formly/core/select'; import { FormlySelectModule } from '@ngx-formly/core/select'; import * as i3$1 from 'ngx-bootstrap/timepicker'; import { TimepickerModule } from 'ngx-bootstrap/timepicker'; import { TextFieldModule } from '@angular/cdk/text-field'; import { FormlyJsonschema } from '@ngx-formly/core/json-schema'; import * as i5$2 from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { TimeSpanInMs, INTERVAL_VALUES, INTERVALS, INTERVAL_TITLES, IntervalPickerComponent } from '@c8y/ngx-components/interval-picker'; import * as i9 from '@angular/cdk/table'; import { CdkTable, CdkHeaderCell, CdkTableModule } from '@angular/cdk/table'; import * as i19 from 'ngx-bootstrap/pagination'; import { PaginationModule } from 'ngx-bootstrap/pagination'; /** * Allows to set a icon. Switches between c8y default icons * and font awesome icons. * ```html * <i [c8yIcon]="'clock'"> * ``` */ class IconDirective { set c8yIcon(icon) { this.updateIcon(icon); } constructor(el, renderer) { this.el = el; this.renderer = renderer; this.c8yMatch = /^c8y-/; this.dltC8yMatch = /^dlt-c8y-/; this.currentClasses = ''; } isC8y(icon) { return this.c8yMatch.test(icon); } isDltC8y(icon) { return this.dltC8yMatch.test(icon); } getClasses(icon) { let classes = ''; if (typeof icon === 'object' && icon?.class) { icon.class = this.mapFontAwesomeToDelightIcons(icon.class); classes = `${this.isC8y(icon.class) ? `${icon.class.replace(this.c8yMatch, 'c8y-icon c8y-icon-')}` : this.isDltC8y(icon.class) ? '' : `c8y-icon dlt-c8y-icon-${icon.class}`} c8y-icon-duocolor`; return classes; } if (icon && typeof icon === 'string') { const _icon = icon.trim(); const isC8y = this.isC8y(_icon); const iconClass = isC8y ? _icon.replace(this.c8yMatch, 'c8y-icon-') : `c8y-icon dlt-c8y-icon-${_icon}`; classes = `${isC8y ? 'c8y-icon' : ''} ${iconClass}`; } return classes; } updateIcon(icon) { const newClasses = this.getClasses(icon); if (newClasses !== this.currentClasses) { this.currentClasses .split(/\s/) .filter(c => c) .forEach(klass => { this.renderer.removeClass(this.el.nativeElement, klass); }); newClasses .split(/\s/) .filter(c => c) .forEach(klass => { this.renderer.addClass(this.el.nativeElement, klass); }); this.currentClasses = newClasses; } } mapFontAwesomeToDelightIcons(iconClasses) { if (!iconClasses) { return; } const regex = /fa-/gi; return iconClasses.replace(regex, 'dlt-c8y-icon-'); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: IconDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: IconDirective, isStandalone: true, selector: "[c8yIcon]", inputs: { c8yIcon: "c8yIcon" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: IconDirective, decorators: [{ type: Directive, args: [{ selector: '[c8yIcon]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { c8yIcon: [{ type: Input }] } }); /* * -----------------------------WARNING--------------------------------- * This file implements all properties of the origin AppOptions * **NEVER** change this file directly. Instead do: * 1. open packages/options/src/ApplicationOptions.ts and add your property there * 2. check in this file if all options are implemented. * * TODO: This file should throw when a property is in the AppOptions but not implemented here. * This seems not to work atm. We need to check why. */ class ApplicationOptions { } /** * Tells how a plugin is scoped. */ var PluginsExportScopes; (function (PluginsExportScopes) { /** * Limit the plugin to the current application. It is imported by default. */ PluginsExportScopes["SELF"] = "self"; /** * Allows to add the plugin to a global scope, meaning it is imported to all applications at the same time. * This is not used at the moment but planned to be implemented in the new branding editor. */ PluginsExportScopes["GLOBAL"] = "global"; /** * Limit the plugin to the current application. The plugin is not imported by default. */ PluginsExportScopes["SELF_OPTIONAL"] = "self-optional"; /** * Like undefined the plugin is available for any private application. */ PluginsExportScopes["DEFAULT"] = ""; })(PluginsExportScopes || (PluginsExportScopes = {})); var GroupFragment; (function (GroupFragment) { GroupFragment["groupType"] = "c8y_DeviceGroup"; GroupFragment["subGroupType"] = "c8y_DeviceSubgroup"; GroupFragment["groupFragmentType"] = "c8y_IsDeviceGroup"; GroupFragment["dataBrokerSourceFragment"] = "c8y_BrokerSource"; GroupFragment["dynamicGroupType"] = "c8y_DynamicGroup"; GroupFragment["dynamicGroupFragment"] = "c8y_IsDynamicGroup"; GroupFragment["dynamicGroupColumnConfig"] = "c8y_UIDeviceFilterConfig"; GroupFragment["dynamicGroupQueryString"] = "c8y_DeviceQueryString"; })(GroupFragment || (GroupFragment = {})); /** * A abstract state which should be included in each state. * @abstract */ class StateService { /** * Maps to a property and just returns that property. * @param mappedProperty The property to map to. */ map(mappedProperty) { return this.state$.pipe(map(mappedProperty), distinctUntilChanged()); } /** * Emits a new state. */ emitNewState() { this.state$.next(this.state); } } function fromTrigger(router, refresh, factories) { return merge(router.events.pipe(filter(evt => evt instanceof NavigationEnd)), ...castArray(refresh)).pipe(startWith(1), switchMap(() => fromFactories(factories, router))); } function fromTriggerOnce(router, refresh, factories) { return merge(...castArray(refresh)).pipe(startWith(1), switchMap(() => fromFactories(factories, router))); } var InjectionType; (function (InjectionType) { InjectionType[InjectionType["COMPONENT"] = 0] = "COMPONENT"; InjectionType[InjectionType["ROUTE"] = 1] = "ROUTE"; })(InjectionType || (InjectionType = {})); class StandalonePluginInjector extends Injector { /** * @deprecated Use `constructor` instead. */ static create(..._args) { throw Error('Not implemented'); } constructor(options) { super(); this.options = options; this.injector = Injector.create(options); } get name() { return this.options.name; } get(token, notFoundValue, options) { return this.injector.get(token, notFoundValue, options); } } function getInjectedHooks(token, injectors, type = InjectionType.COMPONENT) { return () => flatten(injectors.map(injector => { const factoryOrFactories = injector.get(token, [], { self: true }); const factories = Array.isArray(factoryOrFactories) ? flatten(factoryOrFactories) : [factoryOrFactories]; if (injector.scopes?.has('root')) { return factories; } if (injector instanceof StandalonePluginInjector) { // No need to set injector for items retrieved from standalone plugins return factories; } factories.forEach((factory) => { if (!factory.get && factory.injector !== null) { if (type === InjectionType.ROUTE) { factory._injector = injector; } else { factory.injector = injector; } } }); return factories; })); } function fromFactories(factories, router, withFirstEmpty = true) { return !Array.isArray(factories) || factories.length < 1 ? of([]) : defer(() => { const factoryObservables = resolveInjectedFactories(factories).map(f => { if (Array.isArray(f)) { return toObservableOfArrays(f, withFirstEmpty); } if (isExtensionFactory(f)) { return toObservableOfArrays(f.get(getActivatedRoute(router)), withFirstEmpty); } return toObservableOfArrays([f], withFirstEmpty); }); return combineLatest(factoryObservables); }).pipe(map(results => sortByPriority([].concat(...results))), map(value => uniq(value))); } function resolveInjectedFactories(factories) { return flatten(factories.map(f => { if (typeof f === 'function') { const func = f; return func(); } return [f]; })); } function stateToFactory(componentsState) { const components$ = componentsState.pipe(map((componentSet) => [...componentSet])); return { get: () => components$ }; } function sortByPriority(items) { return sortBy(items, item => -(item?.priority || 0)); } function removeDuplicatesIds(items) { const grouped = groupBy(items, 'id'); const itemsWithoutDuplicates = new Array(); for (const key of Object.keys(grouped)) { if (key && key !== 'undefined') { const sortedByPrio = sortByPriority(grouped[key]); itemsWithoutDuplicates.push(sortedByPrio[0]); } else { itemsWithoutDuplicates.push(...grouped[key]); } } return sortByPriority(itemsWithoutDuplicates); } function toObservableOfArrays(factoryResult, withFirstEmpty) { let observable; if (!factoryResult) { return of([]); } else { observable = toObservable(factoryResult); if (withFirstEmpty) { const withEmptyFirst = observable.pipe(startWith([])); observable = race(observable, withEmptyFirst); } } return observable.pipe(map(result => (Array.isArray(result) ? result : [result]).filter(item => !!item))); } function isPromise(obj) { return !!obj && typeof obj.then === 'function'; } function isExtensionFactory(obj) { return !!obj && typeof obj.get === 'function'; } /** * Converts any value provided to an Observable that emits this value once and then completes. * A convenience method to represent all the data as Observables rather than * a mixture of Observables and other types. * * @param value The value the resulting Observable will emit. */ function toObservable(value) { if (isObservable(value)) { return value; } if (isPromise(value)) { return from(value); } return of(value); } class ExtensionPointWithoutStateForPlugins { constructor(rootInjector, pluginService) { this.factories = []; this.refreshTrigger = new Subject(); this.injectors = [rootInjector]; pluginService.injectors$.subscribe(injector => { this.injectors.push(injector); }); this.refresh$ = merge(this.refreshTrigger, pluginService.refresh$); } /** * Refresh the extension factories */ refresh() { this.refreshTrigger.next(); } } class ExtensionPointForPlugins extends StateService { constructor(rootInjector, pluginService) { super(); this.factories = []; this.state$ = new BehaviorSubject(new Set()); this.refreshTrigger = new Subject(); this.injectors = [rootInjector]; pluginService.injectors$.subscribe(injector => { this.injectors.push(injector); }); this.refresh$ = merge(this.refreshTrigger, pluginService.refresh$); } /** * Refresh the extension factories */ refresh() { this.refreshTrigger.next(); } } /** * Helper function to get the activated route in * a service (as ActivatedRoute injection only * works in components). Works as long as we only use * a tree and no child is active at the same time. * * @param router The current router */ function getActivatedRoute(router) { if (router && router.routerState && router.routerState.root) { let route = router.routerState.root; while (route.firstChild) { route = route.firstChild; } return route; } } /** * A generic function to be used by specific implementations of the HOOK concept. * @param items The items that should be provided under the `useValue` or `useClass` attribute. * Allows an extension factory to be passed as an argument, which can create instances of type T. * @param token The InjectionToken/HOOK to be provided. * @param options If this is a multi provider or not (defaults to true) and provider type definition (defaults to ClassProvider) - `HookOptions`. * @returns A `Provider` (either `ValueProvider` or `ClassProvider`) to be provided in a module. */ function hookGeneric(items, token, options) { const finalOptions = Object.assign({ multi: true, providerType: HookProviderTypes.ClassProvider }, options); const { multi, providerType } = finalOptions; if (typeof items !== 'function') { return { provide: token, useValue: items, multi }; } if (providerType === HookProviderTypes.ExistingProvider) { return { provide: token, useExisting: items, multi }; } return { provide: token, useClass: items, multi }; } var HookProviderTypes; (function (HookProviderTypes) { HookProviderTypes["ExistingProvider"] = "ExistingProvider"; HookProviderTypes["ClassProvider"] = "ClassProvider"; })(HookProviderTypes || (HookProviderTypes = {})); function allEntriesAreEqual(previous, next) { if (previous === next) return true; if (previous == null || next == null) return false; if (previous.length !== next.length) return false; for (let i = 0; i < previous.length; ++i) { if (previous[i] !== next[i]) return false; } return true; } /** * @deprecated Consider using the `hookOptions` function instead. */ const HOOK_OPTIONS = new InjectionToken('HOOK_OPTIONS'); /** * You can either provide a single `ApplicationOptions` as parameter: * ```typescript * hookOptions(...) * ``` * * Or an array to directly register multiple: * ```typescript * hookOptions([...]) * ``` * * Or you provide an Service that implements `ExtensionFactory<ApplicationOptions>` * ```typescript * export class MyApplicationOptionsFactory implements ExtensionFactory<ApplicationOptions> {...} * ... * hookOptions(MyApplicationOptionsFactory) * ``` * A typed alternative to `HOOK_OPTIONS`. * @param options The `ApplicationOptions`'s or `ExtensionFactory` to be provided. * @returns An `Provider` to be provided in your module. */ function hookOptions(options, hookOptions) { return hookGeneric(options, HOOK_OPTIONS, hookOptions); } /** * A service that allows to set or get application options * which configure the default behavior of the UI. */ class OptionsService extends ApplicationOptions { constructor(options, systemOptionsService, tenantOptionService) { super(); this.systemOptionsService = systemOptionsService; this.tenantOptionService = tenantOptionService; this._optionsUpdated$ = new Subject(); this.optionsUpdated$ = this._optionsUpdated$.asObservable(); this.setupOptions(options); } /** * Returns an application option used to configure the UI. * @param optionKey The application options key. * @param defaultValue A value to return if non is set. * @param attemptParse Indicates whether the value should be parsed with JSON.parse. */ get(optionKey, defaultValue, attemptParse) { let value = this[optionKey]; if (typeof value === 'undefined') { value = this[camelCase(optionKey)]; } if (attemptParse) { return this.parseOptionRawValue(value, defaultValue); } return typeof value !== 'undefined' ? value : defaultValue; } /** * Returns an observable of an application option used to configure the UI. * @param optionKey The application options key. * @param defaultValue A value to return if non is set. * @param attemptParse Indicates whether the value should be parsed with JSON.parse. */ get$(optionKey, defaultValue, attemptParse) { const fn = () => this.get(optionKey, defaultValue, attemptParse); return this.optionsUpdated$.pipe(map(fn), startWith(fn()), distinctUntilChanged()); } /** * Sets an application option. * @param key The key to set. * @param value The value to set. */ set(key, value) { const camelCasedKey = camelCase(key); const previousValue = this[camelCasedKey]; this[camelCasedKey] = value; if (!isEqual(previousValue, value)) { this._optionsUpdated$.next(); } } /** * Deletes an application option. * @param key The key to remove. */ delete(key) { const camelCasedKey = camelCase(key); const previousValue = this[camelCasedKey]; delete this[camelCasedKey]; const newValue = this[camelCasedKey]; if (!isEqual(previousValue, newValue)) { this._optionsUpdated$.next(); } } /** * Gets support URL from: * - application option `supportUrl`, * - or current tenant's option `configuration / system.support.url`, * - or current tenant's inherited option `configuration / system.support.url`, * - or system option `configuration / system.support.url`, * - otherwise defaults to `false`. * * @returns Returns support url or false. */ async getSupportUrl() { let url = this.supportUrl; if (isUndefined(url)) { url = await this.getCurrentTenantOption('configuration', 'system.support.url'); } if (isUndefined(url)) { url = await this.getInheritedTenantOption('configuration', 'system.support.url'); } if (isUndefined(url)) { url = (await this.getSystemOption('support', 'url')) || false; } this.supportUrl = url; return this.supportUrl; } /** * Returns if the tenant allows to show the activate-support user menu entry. * Note: Only if system-level support-user/enabled is false we can activate it at tenant level. */ async getActivateSupportUser() { const option = await this.getSystemOption('support-user', 'enabled', true); return !option; } /** * Gets a value from the system service and parses it. * * @param category The category for this option. * @param key The key for that option. * @param defaultValue The default if the option was not found. */ async getSystemOption(category, key, defaultValue) { return this.getOptionFromService(category, key, null, this.systemOptionsService, defaultValue); } /** * Gets a value from the tenant service and parses it. * * @param category The category for this option. * @param key The key for that option. * @param defaultValue The default if the option was not found. */ async getTenantOption(category, key, defaultValue) { return this.getOptionFromService(category, key, null, this.tenantOptionService, defaultValue); } /** * Gets an inherited from parent value from the tenant service if inheritance supported based on given parameters. * * @param category The category for this option. * @param key The key for that option. * @param defaultValue The default if the option was not found. */ async getInheritedTenantOption(category, key, defaultValue) { return this.getOptionFromService(category, key, { evaluate: 'inherited' }, this.tenantOptionService, defaultValue); } /** * Gets current tenant option value from the tenant service omitting the inheritance supported based on given parameters. * * @param category The category for this option. * @param key The key for that option. * @param defaultValue The default if the option was not found. */ async getCurrentTenantOption(category, key, defaultValue) { return this.getOptionFromService(category, key, { evaluate: 'current' }, this.tenantOptionService, defaultValue); } setupOptions(options) { if (options) { if (!Array.isArray(options)) { options = [options]; } let combinedOptions = {}; if (options.length === 1) { combinedOptions = options[0]; } else if (options.length > 1) { options.forEach(optionMap => { if (optionMap) { this.applyOptions(optionMap, combinedOptions); } }); } else { return; } if (this.applyOptions(combinedOptions, this)) { this._optionsUpdated$.next(); } } } applyOptions(options, applyTo) { let optionWasChanged = false; Object.entries(options).forEach(([key, value]) => { const camelCasedKey = camelCase(key); const previousValue = applyTo[camelCasedKey]; applyTo[camelCasedKey] = value; if (!isEqual(previousValue, value)) { optionWasChanged = true; } }); return optionWasChanged; } async getOptionFromService(category, key, filter, service, defaultValue) { try { const { data } = await service.detail({ category, key }, filter); return this.parseOptionRawValue(data.value, defaultValue); } catch (ex) { return defaultValue; } } parseOptionRawValue(rawValue, defaultValue) { let value; try { value = JSON.parse(rawValue); } catch (e) { value = isUndefined(rawValue) ? defaultValue : rawValue; } return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OptionsService, deps: [{ token: HOOK_OPTIONS, optional: true }, { token: i1.SystemOptionsService }, { token: i1.TenantOptionsService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OptionsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OptionsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [HOOK_OPTIONS] }] }, { type: i1.SystemOptionsService }, { type: i1.TenantOptionsService }] }); /** * Decorator to throttle functions call. * @param wait throttle time. * @param options set triggers, eg on trailing edge or falling edge, * see lodash documentation for details */ function throttle(wait, options) { return (target, fnName, descriptor) => { const fn = descriptor.value; descriptor.value = function (...args) { const throttledFnName = `_${fnName}Throttled`; if (!this[throttledFnName]) { this[throttledFnName] = throttle$1(fn, wait, options); } return this[throttledFnName](...args); }; }; } class AppStateService extends StateService { constructor(applicationService, apiService, options, fetchClient, tenantLoginOptionsService) { super(); this.applicationService = applicationService; this.apiService = apiService; this.options = options; this.fetchClient = fetchClient; this.tenantLoginOptionsService = tenantLoginOptionsService; /** * Saves the state. Should not be accessible directly. Use map or the getter to access * the state. Use functions in the implementation to change the state. */ this.state$ = new BehaviorSubject({ app: { name: this.options.name, contextPath: this.getCurrentContextPath() || this.options.contextPath, icon: this.options.icon }, supportUrl: this.options.supportUrl, lang: this.options.get('defaultLanguage', 'en'), langs: this.getLangs(), langsDetail: this.options.languages, loginOptions: this.options.loginOptions, activateSupportUserAvailable: undefined, versions: { backend: undefined, ui: this.options.versions || { ngx: undefined } }, hidePowered: this.options.hidePowered, isLoading: false, showRightDrawer: this.options.rightDrawer, loginExtraLink: this.options.get('login_extra_link'), newsletter: this.options.newsletter }); this.currentSupportUserName = new BehaviorSubject(null); this.currentUser = new BehaviorSubject(null); this.currentTenant = new BehaviorSubject(null); this.currentApplication = new BehaviorSubject(null); this.currentApplicationConfig = this.currentApplication.pipe(filter(app => !!app), map(app => app?.config || null)); // in case of noLogin being truthy `loadManifest` is never called. if (this.options.noLogin) { this.currentApplication.next(this.state.app); } this.apiService.isLoading$.subscribe(isLoading => { this.state.isLoading = isLoading; }); this.assignApplicationKeyToDefaultHeaders(); this.currentAppsOfUser = this.currentAppsOfUser$(); } assignApplicationKeyToDefaultHeaders() { if (!isDevMode()) { this.fetchClient.defaultHeaders = { ...(this.fetchClient.defaultHeaders || {}), 'X-Cumulocity-Application-Key': this.options.key }; } } /** * Returns the current state. */ get state() { return this.state$.value; } getLangs() { const { languages } = this.options; return languages ? keys(languages).filter(k => languages[k]) : []; } /** * Returns the correct UI version. In hybrid mode for angular and ngx. */ get uiVersion() { const version = this.state.versions.ui; return version.ngx || version.ng1; } /** * Loads the app manifest. If no access -> throw an error to verify app access. */ async loadManifest() { try { const normalizedContextPath = this.state.app.contextPath?.split('@')[0]; const { data: application } = await this.applicationService.getManifestOfContextPath(normalizedContextPath); this.state.app.manifest = application; this.state.app.id = application.id; const { data } = await this.applicationService.detail(application.id); this.currentApplication.next(data); await this.loadDefaultOptions(); } catch (ex) { this.currentApplication.next(this.state.app); throw ex; } } /** * Dynamic options are stored on the API in a specific config: {} object. They can * be used to configure the app dynamically. * * Note: To avoids conflicts with the default Config, it is recommended * to use a certain namespace. */ async updateCurrentApplicationConfig(config) { const appWithUpdatedConfig = await this.applicationService.updateApplicationConfig(this.state.app.id, config); this.currentApplication.next(appWithUpdatedConfig); return appWithUpdatedConfig.config; } /** * When this function called, it refreshes the values of loginOptions stored within ui state object. * Function is throttled to execute the refresh once in a time specified by params of @throttled decorator, * it should be called on leading edge of the timeout. */ async refreshLoginOptions() { const loginOptions = (await this.tenantLoginOptionsService.listForCurrentTenant()).data; this.state$.next({ ...this.state, loginOptions }); } /** * Checks current users application list and matches it against given application name. * Returns true if application is in the list. * @param name application name */ async isApplicationAvailable(name) { const apps = await this.currentAppsOfUser.pipe(take(1)).toPromise(); return apps.some(app => app.name === name || app.contextPath === name); } /** * Sets current user (including support user). * @param userInfo Info about current user and support user to be set. */ setUser(userInfo) { this.currentSupportUserName.next(userInfo.supportUserName || null); this.currentUser.next(userInfo.user); } /** * Verifies if the current application is owned by the current tenant. * @param app The application to verify. * @returns true if it belongs to the current tenant. */ isOwnerOfApplication(app) { if (!app) { app = this.currentApplication.value; } const currentTenant = this.currentTenant.value; const appOwner = get(app, 'owner.tenant.id'); return currentTenant?.name === appOwner; } /** * Verifies if the current application is owned by the current tenant. * @param app The application to verify. * @returns true if it belongs to the current tenant. */ isOwnerOfApplication$(app) { const app$ = app ? of(app) : this.currentApplication; return combineLatest([app$, this.currentTenant]).pipe(map(([app, tenant]) => { if (!app || !tenant) { return false; } return tenant.name === get(app, 'owner.tenant.id'); })); } /** * @returns The current contextPath. */ getCurrentContextPath() { const match = window.location.pathname.match(/\/apps\/(public\/){0,1}(.+?)(\/|\?|#|$)/); return match && match[2]; } currentAppsOfUser$() { const appChanges$ = this.onAppChangesCompletion$().pipe(startWith(undefined)); const userChanges$ = this.currentUser.pipe(map(user => user?.id), distinctUntilChanged()); return combineLatest([userChanges$, appChanges$]).pipe(filter(([userId]) => !!userId), switchMap(([userId]) => this.applicationService.listByUser(userId, { dropOverwrittenApps: true, noPaging: true })), map(({ data }) => data), shareReplay({ bufferSize: 1, refCount: true })); } /** * An Observable emitting once all POST, PUT, DELETE requests to the application API finished */ onAppChangesCompletion$() { const methods = ['POST', 'PUT', 'DELETE']; return this.apiService.calls.pipe(filter(({ method, url }) => methods.includes(method) && url?.includes('application/applications')), map(({ phase }) => (phase === 'start' ? 1 : -1)), scan((count, item) => count + item, 0), map(count => count === 0), distinctUntilChanged(), debounceTime(500), filter(completed => !!completed), map(() => { return; })); } async loadDefaultOptions() { this.state.supportUrl = await this.options.getSupportUrl(); this.state.activateSupportUserAvailable = await this.options.getActivateSupportUser(); this.state.versions.backend = await this.options.getSystemOption('system', 'version'); this.emitNewState(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppStateService, deps: [{ token: i1.ApplicationService }, { token: i4.ApiService }, { token: OptionsService }, { token: i1.FetchClient }, { token: i1.TenantLoginOptionsService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppStateService, providedIn: 'root' }); } } __decorate([ throttle(600, { trailing: false }), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], AppStateService.prototype, "refreshLoginOptions", null); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppStateService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.ApplicationService }, { type: i4.ApiService }, { type: OptionsService }, { type: i1.FetchClient }, { type: i1.TenantLoginOptionsService }], propDecorators: { refreshLoginOptions: [] } }); /** * A wrapper class for handling realtime notifications in RxJS fashion. */ class RealtimeService { /** * A flag displaying if realtime notifications are currently active. */ get active() { return this.isActive.value; } /** * An observable emitting a value in case the realtime connection has been interrupted. * Can be used to reload data of e.g. a datapoint graph that wasn't received while realtime was interrupted. */ get reconnect$() { return this.realtimeSubject.reconnect$; } /** * An observable emitting either `connected` or `disconnected` depending on the state of the realtime connection. * Can be used to e.g. inform the user about the interrupted realtime connection. */ get connectionStatus$() { return this.realtimeSubject.connectionStatus$; } constructor(realtimeSubject) { this.realtimeSubject = realtimeSubject; this.isActive = new BehaviorSubject(true); } /** * Get an Observable of all realtime notifications. * * @param {string | number | IIdentified} entityOrId Entity object or id * * @returns An [[Observable]] of notifications wrapped as [[RealtimeMessage]] */ onAll$(entityOrId) { const subject$ = this.realtimeSubject.getObservableForChannel(this.getChannel(entityOrId)); return this.isActive.pipe(switchMap(active => (active ? subject$ : NEVER))); } /** * Subscribes again all realtime channels with active observers. */ start() { if (!this.active) { this.isActive.next(true); } } /** * Stops realtime notifications and unsubscribes all realtime channels. */ stop() { if (this.active) { this.isActive.next(false); } } /** * Get an Observable of all CREATE realtime notifications. * * @param {string | number | IIdentified} entityOrId Entity object or id * * @returns An [[Observable]] of newly created entity objects. */ onCreate$(entityOrId) { return this.onAll$(entityOrId).pipe(filter(msg => msg.realtimeAction === 'CREATE'), map(msg => msg.data)); } /** * Get an Observable of all UPDATE realtime notifications. * * @param {string | number | IIdentified} entityOrId Entity object or id * * @returns An [[Observable]] of updated entity objects. */ onUpdate$(entityOrId) { return this.onAll$(entityOrId).pipe(filter(msg => msg.realtimeAction === 'UPDATE'), map(msg => msg.data)); } /** * Get an Observable of all DELETE realtime notifications. * * @param {string | number | IIdentified} entityOrId Entity object or id * * @returns An [[Observable]] of deleted entity objects. */ onDelete$(entityOrId) { return this.onAll$(entityOrId).pipe(filter(msg => msg.realtimeAction === 'DELETE'), map(msg => coerceNumberProperty(msg.data))); } getIdString(reference) { let id; if (typeof reference === 'object') { id = reference.id; } else { id = reference; } return String(id); } getChannel(entityOrId) { return entityOrId ? this.channel().replace('*', this.getIdString(entityOrId)) : this.channel(); } } /** * Service (providedIn root) that ensures to only create a single realtime subscription for each channel */ class RealtimeSubjectService { constructor(realtime) { this.realtime = realtime; this.subjects$ = new Map(); this.reconnect$ = this.createObservableForReconnect().pipe(share()); this.connectionStatus$ = this.createObservableForConnectionStatus().pipe(distinctUntilChanged(), shareReplay({ refCount: true, bufferSize: 1 })); } getObservableForChannel(channel) { if (this.subjects$.has(channel)) { return this.subjects$.get(channel); } const observable$ = this.createObservableForChannel(channel, this.realtime); const sharedObservable$ = observable$.pipe(share()); this.subjects$.set(channel, sharedObservable$); return sharedObservable$; } createObservableForChannel(channel, realtime) { return new Observable(observer => { let realtimeSubscription = realtime.subscribe(channel, msg => { const data = { channel: msg.channel, data: msg.data.data, id: msg.id, realtimeAction: msg.data.realtimeAction }; observer.next(data); }); /** * In (rare) case of a re-handshake, resubscribe valid subscriptions. * @see https://docs.cometd.org/current/reference/#_javascript_subscribe_resubscribe */ const reconnectSubscription = this.reconnect$.subscribe(() => { try { realtimeSubscription = this.realtime.resubscribe(realtimeSubscription); } catch (e) { console.warn(`Failed to resubscribe to channel: "${channel}" after reconnect.`, e); observer.error(e); } }); return { unsubscribe: () => { reconnectSubscription.unsubscribe(); realtime.unsubscribe(realtimeSubscription); } }; }); } createObservableForReconnect() { return new Observable(observer => { const handle = this.realtime.addHandshakeListener(msg => { if (msg.successful && msg.reestablish) { observer.next(); } }); return { unsubscribe: () => { this.realtime.removeListener(handle); } }; }); } createObservableForConnectionStatus() { return new Observable(observer => { observer.next(!this.realtime.isDisconnected() ? 'connected' : 'disconnected'); const handle = this.realtime.addConnectListener(msg => { observer.next(msg.successful ? 'connected' : 'disconnected'); }); return { unsubscribe: () => { this.realtime.removeListener(handle); } }; }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RealtimeSubjectService, deps: [{ token: i1.Realtime }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RealtimeSubjectService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RealtimeSubjectService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.Realtime }] }); class ManagedObjectRealtimeService extends RealtimeService { constructor(realtimeSubject) { super(realtimeSubject); this.realtimeSubject = realtimeSubject; } /** * Get an Observable of all CREATE realtime notifications. * * @returns An [[Observable]] of newly created entity objects. */ onCreate$() { return super.onCreate$(); } channel() { return '/managedobjects/*'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ManagedObjectRealtimeService, deps: [{ token: RealtimeSubjectService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ManagedObjectRealtimeService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ManagedObjectRealtimeService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: RealtimeSubjectService }] }); /** * AssetTypesRealtimeService is being used to manage a cache of all existing asset types. * This service is injected in the AssetOverviewNavigationFactory class, which will trigger * the initialization of the cache as the constructor is called. */ class AssetTypesRealtimeService { constructor(inventory, appStateService, realtimeSubject) { this.inventory = inventory; this.appStateService = appStateService; this.realtimeSubject = realtimeSubject; this.DEFAULT_ASSET_ICON = 'c8y-enterprise'; this.refreshTrigger = new Subject(); this.managedObjectRealtimeService = new ManagedObjectRealtimeService(this.realtimeSubject); this.assetTypes$ = this.initAssetTypes$(); } /** * Returns an asset type from the cache based on the unique name property. * @param name Name of the asset type. * @returns IManagedObject which represents the asset type. */ getAssetTypeByName$(name) { return this.assetTypes$.pipe(map(assetTypes => assetTypes[name])); } /** * Returns an asset type from the cache based on the id. * @param assetTypeId Id of the asset type. * @returns IManagedObject which represents the asset type. */ getAssetTypeById$(assetTypeId) { return this.assetTypes$.pipe(map(assetTypes => Object.values(assetTypes).find((assetType) => assetType.id === assetTypeId))); } /** * Returns all the available asset types from the cache. * @returns available as