@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,157 lines (1,145 loc) • 2.32 MB
JavaScript
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