@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,299 lines (1,286 loc) • 630 kB
JavaScript
import { gettext } from '@c8y/ngx-components/gettext';
import { aggregationType } from '@c8y/client';
import * as i0 from '@angular/core';
import { computed, signal, Injectable, inject, input, forwardRef, ChangeDetectionStrategy, Component, DestroyRef, output, Input, ChangeDetectorRef, ViewChild, EventEmitter, viewChild, effect, HostListener, Output, TemplateRef, untracked, ContentChild, NgModule } from '@angular/core';
import * as i4 from '@c8y/ngx-components';
import { ViewContext, I18nModule, IconDirective, FormGroupComponent, MessagesComponent, MessageDirective, DateTimePickerModule, C8yTranslatePipe, DatePipe, TabsOutletComponent, FormsModule as FormsModule$1, CountdownIntervalModule, CountdownIntervalComponent, AlertService, ContextRouteService, DashboardChildComponent, hookActionBar } from '@c8y/ngx-components';
import { BehaviorSubject, map, distinctUntilChanged, shareReplay, fromEvent, filter, debounceTime, startWith, tap as tap$1, firstValueFrom, combineLatest, Subject, take, skip, throttleTime, merge as merge$1, EMPTY } from 'rxjs';
import * as i1 from '@angular/forms';
import { FormBuilder, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormControl } from '@angular/forms';
import { pick, omit, isObject, isEqual, transform, some, cloneDeep, merge, isEmpty } from 'lodash-es';
import { ActivatedRoute, Router, NavigationStart, NavigationEnd, ActivationEnd } from '@angular/router';
import * as i1$1 from '@angular/common';
import { NgClass, CommonModule, AsyncPipe, TitleCasePipe, Location, NgTemplateOutlet } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ContextDashboardStateService } from '@c8y/ngx-components/context-dashboard-state';
import * as i2 from 'ngx-bootstrap/tooltip';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import * as i1$2 from 'ngx-bootstrap/collapse';
import { CollapseModule } from 'ngx-bootstrap/collapse';
import * as i2$2 from 'ngx-bootstrap/popover';
import { PopoverModule } from 'ngx-bootstrap/popover';
import * as i2$1 from 'ngx-bootstrap/dropdown';
import { BsDropdownModule, BsDropdownDirective } from 'ngx-bootstrap/dropdown';
import { A11yModule } from '@angular/cdk/a11y';
import { withLatestFrom, filter as filter$1, tap, distinctUntilChanged as distinctUntilChanged$1, map as map$1, catchError } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
const GLOBAL_CONTEXT_EVENTS = {
STATE_CHANGE: 'GLOBAL_CONTEXT_STATE_CHANGE',
REFRESH: 'REFRESH',
REFRESH_OPTION_CHANGE: 'GLOBAL_CONTEXT_REFRESH_OPTION_CHANGE',
UPDATE_GLOBAL_CONTEXT_LIVE: 'UPDATE_GLOBAL_CONTEXT_LIVE',
UPDATE_GLOBAL_CONTEXT_HISTORY: 'UPDATE_GLOBAL_CONTEXT_HISTORY',
INIT_GLOBAL_CONTEXT: 'INIT_GLOBAL_CONTEXT'
};
var GLOBAL_CONTEXT_DISPLAY_MODE;
(function (GLOBAL_CONTEXT_DISPLAY_MODE) {
GLOBAL_CONTEXT_DISPLAY_MODE["DASHBOARD"] = "dashboard";
GLOBAL_CONTEXT_DISPLAY_MODE["CONFIG"] = "config";
GLOBAL_CONTEXT_DISPLAY_MODE["VIEW_AND_CONFIG"] = "view_and_config";
})(GLOBAL_CONTEXT_DISPLAY_MODE || (GLOBAL_CONTEXT_DISPLAY_MODE = {}));
var DateContextQueryParamNames;
(function (DateContextQueryParamNames) {
DateContextQueryParamNames["DATE_CONTEXT_FROM"] = "dateContextFrom";
DateContextQueryParamNames["DATE_CONTEXT_TO"] = "dateContextTo";
DateContextQueryParamNames["DATE_CONTEXT_INTERVAL"] = "dateContextInterval";
DateContextQueryParamNames["DATE_CONTEXT_AGGREGATION"] = "dateContextAggregation";
DateContextQueryParamNames["DATE_CONTEXT_AUTO_REFRESH"] = "globalContextAutoRefresh";
DateContextQueryParamNames["DATE_CONTEXT_REFRESH_MODE"] = "globalContextRefreshMode";
})(DateContextQueryParamNames || (DateContextQueryParamNames = {}));
const GLOBAL_CONTEXT_SOURCE = {
WIDGET: 'widget',
DASHBOARD: 'dashboard'
};
const WIDGET_DISPLAY_MODE = {
INLINE: 'inline',
CONFIG: 'config',
PREVIEW: 'preview'
};
const REFRESH_OPTION = {
LIVE: 'live',
HISTORY: 'history'
};
const todayDate = new Date();
const TIME_SPAN_MS = {
MINUTE: 1000 * 60,
HOUR: 1000 * 60 * 60,
DAY: 1000 * 60 * 60 * 24,
WEEK: 1000 * 60 * 60 * 24 * 7,
MONTH: todayDate.valueOf() - new Date(todayDate.setMonth(todayDate.getMonth() - 1)).valueOf()
};
const TIME_INTERVAL = {
NONE: 'none',
MINUTES: 'minutes',
HOURS: 'hours',
DAYS: 'days',
WEEKS: 'weeks',
MONTHS: 'months',
CUSTOM: 'custom'
};
const INTERVALS = [
{
id: 'minutes',
title: gettext('Last minute'),
timespanInMs: TIME_SPAN_MS.MINUTE
},
{
id: 'hours',
title: gettext('Last hour'),
timespanInMs: TIME_SPAN_MS.HOUR
},
{
id: 'days',
title: gettext('Last day'),
timespanInMs: TIME_SPAN_MS.DAY
},
{
id: 'weeks',
title: gettext('Last week'),
timespanInMs: TIME_SPAN_MS.WEEK
},
{
id: 'months',
title: gettext('Last month'),
timespanInMs: TIME_SPAN_MS.MONTH
},
{ id: 'custom', title: gettext('Custom') }
];
const INTERVAL_TITLES = {
none: gettext('No date filter'),
minutes: gettext('Last minute'),
hours: gettext('Last hour'),
days: gettext('Last day'),
weeks: gettext('Last week'),
months: gettext('Last month'),
custom: gettext('Custom')
};
/**
* Default values for global context configuration
* Single source of truth for all default values across the global context feature
*/
const GLOBAL_CONTEXT_DEFAULTS = {
/** Default refresh option - live mode */
REFRESH_OPTION: REFRESH_OPTION.LIVE,
/** Default auto-refresh state */
IS_AUTO_REFRESH_ENABLED: true,
/** Default refresh interval in milliseconds (5 seconds) */
REFRESH_INTERVAL: 5000,
/** Default aggregation type */
AGGREGATION: null,
/** Default display mode */
DISPLAY_MODE: GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD,
/** Default date range duration in milliseconds (1 hour) */
DATE_RANGE_DURATION_MS: 60 * 60 * 1000,
/** Default time interval for custom ranges */
TIME_INTERVAL: TIME_INTERVAL.HOURS
};
/**
* Define the mapping between GlobalContextState keys and their equivalent in GlobalContextSettings
*/
const LINK_BTNS_CONFIG = {
dateTimeContext: {
formControlName: 'dateTimeContext',
settingKey: 'showTimeContext',
label: gettext('date/time'),
cssClass: 'time-context',
icon: 'calendar'
},
isAutoRefreshEnabled: {
formControlName: 'isAutoRefreshEnabled',
settingKey: 'showAutoRefresh',
label: gettext('auto refresh'),
cssClass: 'auto-refresh',
icon: 'refresh'
},
aggregation: {
formControlName: 'aggregation',
settingKey: 'showAggregation',
label: gettext('aggregation'),
cssClass: 'aggregation',
icon: 'input'
}
};
const AGGREGATIONS = [
{ id: null, title: gettext('None') },
{ id: aggregationType.MINUTELY, title: gettext('Minutely') },
{ id: aggregationType.HOURLY, title: gettext('Hourly') },
{ id: aggregationType.DAILY, title: gettext('Daily') }
];
const AGGREGATION_LIMITS = {
MINUTELY_LIMIT: TIME_SPAN_MS.MINUTE * 10,
HOURLY_LIMIT: TIME_SPAN_MS.DAY * 1,
DAILY_LIMIT: TIME_SPAN_MS.DAY * 4
};
const AGGREGATION_ICON_TYPE = {
UNDEFINED: 'line-chart',
MINUTELY: 'hourglass',
HOURLY: 'clock-o',
DAILY: 'calendar-o'
};
const AGGREGATION_ICONS = {
undefined: AGGREGATION_ICON_TYPE.UNDEFINED,
MINUTELY: AGGREGATION_ICON_TYPE.MINUTELY,
HOURLY: AGGREGATION_ICON_TYPE.HOURLY,
DAILY: AGGREGATION_ICON_TYPE.DAILY
};
const AGGREGATION_TEXTS = {
disabled: gettext('No aggregation with realtime enabled'),
undefined: gettext('No aggregation'),
null: gettext('No aggregation'),
MINUTELY: gettext('Minutely aggregation'),
HOURLY: gettext('Hourly aggregation'),
DAILY: gettext('Daily aggregation')
};
const AGGREGATION_VALUES = {
none: 'NONE',
minutely: aggregationType.MINUTELY,
hourly: aggregationType.HOURLY,
daily: aggregationType.DAILY
};
const AGGREGATION_VALUES_ARR = [
AGGREGATION_VALUES.none,
AGGREGATION_VALUES.minutely,
AGGREGATION_VALUES.hourly,
AGGREGATION_VALUES.daily
];
const AGGREGATION_LABELS = {
NONE: AGGREGATIONS[0].title,
[aggregationType.MINUTELY]: AGGREGATIONS[1].title,
[aggregationType.HOURLY]: AGGREGATIONS[2].title,
[aggregationType.DAILY]: AGGREGATIONS[3].title
};
/**
* Time duration constants (ms)
*/
const TIME_DURATION = {
/** One hour in milliseconds */
ONE_HOUR_MS: 60 * 60 * 1000,
/** One minute in milliseconds */
ONE_MINUTE_MS: 60 * 1000,
/** One second in milliseconds */
ONE_SECOND_MS: 1000
};
/**
* Timing constants for debouncing and throttling
*/
const TIMING = {
/** Debounce time for form value changes (ms) */
FORM_DEBOUNCE: 100,
/** Throttle time for context changes (ms) */
CONTEXT_THROTTLE: 200,
/** Tooltip delay time (ms) */
TOOLTIP_DELAY: 500,
/** Default debounce time (ms) */
DEFAULT_DEBOUNCE: 100
};
/**
* UI priority constants
*/
const UI_PRIORITIES = {
/** High priority for configuration controls */
CONFIGURATION_PRIORITY: 1000
};
/**
* Central utility for all DateTimeContext operations.
* Single source of truth for date validation, conversion, and context normalization.
*/
class DateTimeContextUtil {
/**
* Converts a value to a Date object, handling various input types.
* Allows JavaScript's natural type coercion for compatibility with existing tests.
*/
static toDate(value) {
// Handle undefined explicitly - new Date(undefined) is Invalid Date
if (value === undefined) {
return null;
}
// Already a Date object
if (value instanceof Date) {
return isNaN(value.getTime()) ? null : value;
}
// Let JavaScript's natural type coercion handle all other cases
// This allows null, false, true, numbers, strings to be handled like new Date() would
const date = new Date(value);
return isNaN(date.getTime()) ? null : date;
}
/**
* Converts a date value to ISO string format.
*/
static toIso(value) {
const date = this.toDate(value);
return date ? date.toISOString() : null;
}
/**
* Normalizes an ISO string to remove milliseconds (sets to .000Z).
* This ensures date comparisons ignore millisecond precision.
*
* @param isoString - ISO date string (e.g., "2025-10-27T15:10:04.405Z")
* @returns ISO string with milliseconds zeroed (e.g., "2025-10-27T15:10:04.000Z")
*/
static normalizeIsoToSeconds(isoString) {
if (!isoString || typeof isoString !== 'string') {
return null;
}
// Parse the ISO string to a Date
const date = this.toDate(isoString);
if (!date) {
return null;
}
// Set milliseconds to 0
date.setMilliseconds(0);
return date.toISOString();
}
/**
* Validates if a single date value is valid.
* Only accepts Date objects and valid date strings (not numbers, null, etc.)
* This is the strict validation used for form inputs and user-facing APIs.
*/
static isValidDate(value) {
// Only accept Date instances or strings
if (value instanceof Date) {
return !isNaN(value.getTime());
}
if (typeof value === 'string') {
const date = new Date(value);
return !isNaN(date.getTime());
}
return false;
}
/**
* Validates if a value can be converted to a valid date.
* Uses JavaScript's natural type coercion, accepting null, numbers, booleans, etc.
* This is used internally for range validation.
*/
static isCoercibleToDate(value) {
return this.toDate(value) !== null;
}
/**
* Validates if a date range is valid (from < to).
*/
static isValidRange(from, to) {
const fromDate = this.toDate(from);
const toDate = this.toDate(to);
// Both dates must be valid and fromDate must be before toDate
return fromDate !== null && toDate !== null && fromDate < toDate;
}
/**
* Validates if a date range is valid, allowing equal dates (from <= to).
*/
static isValidRangeOrEqual(from, to) {
const fromDate = this.toDate(from);
const toDate = this.toDate(to);
// Both dates must be valid and fromDate must be before or equal to toDate
return fromDate !== null && toDate !== null && fromDate <= toDate;
}
/**
* Calculates date range from an interval.
* Pure implementation based on INTERVALS configuration.
*/
static dateRangeFromInterval(interval, now = new Date()) {
// Special case: 'none' returns epoch to current time
if (interval === 'none') {
return [new Date(0), now];
}
// Special case: 'custom' has no default range
if (interval === TIME_INTERVAL.CUSTOM) {
return null;
}
// Find the interval configuration
const intervalConfig = INTERVALS.find(({ id }) => id === interval);
if (!intervalConfig || !intervalConfig.timespanInMs) {
return null;
}
// Calculate date range: current time minus interval timespan
const dateTo = new Date(now);
const dateFrom = new Date(dateTo.valueOf() - intervalConfig.timespanInMs);
return [dateFrom, dateTo];
}
/**
* Ensures all dates in a DateTimeContext are ISO strings with milliseconds normalized to .000Z.
*/
static ensureIsoContext(ctx) {
const result = {
interval: ctx.interval || TIME_INTERVAL.CUSTOM,
dateFrom: null,
dateTo: null
};
if (ctx.dateFrom) {
const iso = this.toIso(ctx.dateFrom);
result.dateFrom = this.normalizeIsoToSeconds(iso);
}
if (ctx.dateTo) {
const iso = this.toIso(ctx.dateTo);
result.dateTo = this.normalizeIsoToSeconds(iso);
}
return result;
}
/**
* Normalizes a DateTimeContext for live mode.
* - For non-custom intervals: recomputes range based on current time
* - For custom interval: updates dateTo to current time
*/
static normalizeForLive(ctx, now = new Date()) {
const interval = ctx.interval || TIME_INTERVAL.CUSTOM;
if (interval !== TIME_INTERVAL.CUSTOM) {
// Recompute range from interval
const range = this.dateRangeFromInterval(interval, now);
if (range) {
return this.ensureIsoContext({
interval,
dateFrom: range[0],
dateTo: range[1]
});
}
}
// For custom interval, keep dateFrom but update dateTo to now
return this.ensureIsoContext({
interval: TIME_INTERVAL.CUSTOM,
dateFrom: ctx.dateFrom,
dateTo: now
});
}
/**
* Normalizes a DateTimeContext for history mode.
* Forces interval to CUSTOM and preserves explicit date range.
*/
static normalizeForHistory(ctx) {
return this.ensureIsoContext({
interval: TIME_INTERVAL.CUSTOM,
dateFrom: ctx.dateFrom,
dateTo: ctx.dateTo
});
}
/**
* Type guard to check if a value is a valid DateTimeContext object.
*/
static isValidDateTimeContext(context) {
if (!context || typeof context !== 'object')
return false;
const ctx = context;
const hasDates = 'dateFrom' in ctx && 'dateTo' in ctx && 'interval' in ctx;
if (!hasDates)
return false;
return (typeof ctx.interval === 'string' &&
ctx.interval.length > 0 &&
this.isValidDate(ctx.dateFrom) &&
this.isValidDate(ctx.dateTo));
}
/**
* Checks if a partial DateTimeContext has all required fields.
* Business logic: Allow single field updates or complete updates (all 3 fields),
* but reject incomplete updates with 2 fields.
*/
static isCompleteDateTimeContext(diff) {
// If no dateTimeContext in diff, it's considered complete
if (!('dateTimeContext' in diff) || !diff.dateTimeContext) {
return true;
}
const keys = Object.keys(diff.dateTimeContext);
// Allow single field updates (e.g., just interval) or complete updates (all 3 fields)
// Reject incomplete updates with 2 fields
return keys.length === 1 || keys.length === 3;
}
/**
* Formats a context for emission, ensuring ISO dates and filtering by supported fields.
*/
static formatForEmission(context, supports) {
if (!supports.includes('dateTimeContext') || !context.dateTimeContext) {
return {};
}
const dateTimeContext = context.dateTimeContext;
// For live mode with custom interval, default dateTo to now if missing
if (dateTimeContext.interval === TIME_INTERVAL.CUSTOM && !dateTimeContext.dateTo) {
return {
dateTimeContext: this.ensureIsoContext({
...dateTimeContext,
dateTo: new Date()
})
};
}
return {
dateTimeContext: this.ensureIsoContext(dateTimeContext)
};
}
}
/**
* Creates a complete default global context state
*
* @returns A complete GlobalContextState with all required properties set to sensible defaults
*/
function createDefaultGlobalContextState() {
const now = new Date();
const dateFrom = new Date(now.getTime() - GLOBAL_CONTEXT_DEFAULTS.DATE_RANGE_DURATION_MS);
return {
dateTimeContext: {
dateFrom: dateFrom.toISOString(),
dateTo: now.toISOString(),
interval: GLOBAL_CONTEXT_DEFAULTS.TIME_INTERVAL
},
refreshOption: GLOBAL_CONTEXT_DEFAULTS.REFRESH_OPTION,
isAutoRefreshEnabled: GLOBAL_CONTEXT_DEFAULTS.IS_AUTO_REFRESH_ENABLED,
refreshInterval: GLOBAL_CONTEXT_DEFAULTS.REFRESH_INTERVAL,
aggregation: GLOBAL_CONTEXT_DEFAULTS.AGGREGATION,
displayMode: GLOBAL_CONTEXT_DEFAULTS.DISPLAY_MODE
};
}
/**
* Fills missing or invalid properties in a partial global context state with defaults
*
* @param state - Partial global context state that may have missing or invalid values
* @returns Complete GlobalContextState with all required properties
*/
function fillMissingDefaults(state) {
const defaults = createDefaultGlobalContextState();
// Use central utility to ensure ISO formatting for dateTimeContext
const dateTimeContext = state.dateTimeContext
? DateTimeContextUtil.ensureIsoContext({
...defaults.dateTimeContext,
...state.dateTimeContext,
// Use defaults for invalid dates
dateFrom: DateTimeContextUtil.isValidDate(state.dateTimeContext?.dateFrom)
? state.dateTimeContext.dateFrom
: defaults.dateTimeContext.dateFrom,
dateTo: DateTimeContextUtil.isValidDate(state.dateTimeContext?.dateTo)
? state.dateTimeContext.dateTo
: defaults.dateTimeContext.dateTo,
interval: state.dateTimeContext?.interval || defaults.dateTimeContext.interval
})
: defaults.dateTimeContext;
return {
...defaults,
...state,
dateTimeContext,
// Always override refreshInterval to use the fixed 5s default
// This ensures old dashboards with custom intervals are migrated to the new fixed interval
refreshInterval: GLOBAL_CONTEXT_DEFAULTS.REFRESH_INTERVAL
};
}
/**
* Validates if a value is a valid date (either Date object or ISO string)
* Delegates to central utility for consistency.
*
* @param value - Value to validate (string, Date, or undefined)
* @returns true if the value represents a valid date
*/
function isValidDateValue(value) {
return DateTimeContextUtil.isValidDate(value);
}
/**
* Checks if a date time context object has all required valid properties
* Delegates to central utility for consistency.
*
* @param context - DateTimeContext to validate
* @returns true if the context is complete and valid
*/
function isValidDateTimeContext(context) {
return DateTimeContextUtil.isValidDateTimeContext(context);
}
/**
* Checks if a dateTimeContext diff object is complete (has all 3 required fields).
* Delegates to central utility for consistency.
*
* @param diff - Partial state that may contain dateTimeContext
* @returns true if dateTimeContext is absent, has only 1 field, or has all 3 fields; false if it has 2 fields (incomplete)
*/
function isCompleteDateTimeContext(diff) {
return DateTimeContextUtil.isCompleteDateTimeContext(diff);
}
/**
* Formats dateTimeContext for emission, handling date conversions and defaults.
* Delegates to central utility for consistency.
*
* @param context - Global context state to format
* @param supports - Array of supported fields from widget template
* @returns Formatted context with properly formatted dateTimeContext
*/
function formatDateTimeContextForEmission(context, supports) {
const result = DateTimeContextUtil.formatForEmission(context, supports);
// Merge back with original context to preserve other fields
return {
...context,
...result
};
}
/**
* Default settings applied when no widgets have registered any settings.
*/
const DEFAULT_GLOBAL_CONTEXT_SETTINGS = {
showTimeContext: false,
showAggregation: false,
showAutoRefresh: false,
showRefresh: false,
showRefreshInterval: false
};
/**
* GlobalContextService manages widget settings registration and loading states.
*
* This service is responsible for:
* - Registering and managing widget-specific global context settings
* - Consolidating settings from multiple widgets using OR logic
* - Tracking loading states across multiple widgets
* - Providing default configurations for the global context
*
* @example
* ```typescript
* // Register widget settings
* globalContextService.register('widget-1', { showTimeContext: true });
*
* // Get consolidated settings from all widgets
* globalContextService.getConsolidatedSettings().subscribe(settings => {
* console.log(settings); // { showTimeContext: true, ... }
* });
*
* // Track loading state
* globalContextService.registerLoading('widget-1');
* console.log(globalContextService.isLoading()); // true
* ```
*/
class GlobalContextService {
constructor() {
/**
* Computed signal that indicates if any widgets are currently in a loading state.
* Returns true if at least one widget has registered a loading state.
*/
this.isLoading = computed(() => this.loadingIds().size > 0, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
/** Target context for the global context service */
this.targetContext = ViewContext.Group;
/** Observable stream of all registered widget settings */
this.settings$ = new BehaviorSubject([]);
/** Signal storing unique identifiers of widgets currently in loading state */
this.loadingIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "loadingIds" }] : []));
}
/**
* Registers global context settings for a specific widget.
* If settings for the same widget ID already exist, they will be updated.
*
* @param id - Unique identifier for the widget
* @param settings - Partial global context settings to register
*
* @throws {Error} When id is empty or null
*
* @example
* ```typescript
* globalContextService.register('my-widget', {
* showTimeContext: true,
* showAutoRefresh: false
* });
* ```
*/
register(id, settings) {
this.validateWidgetId(id);
const registeredSettings = {
...settings,
id
};
const currentSettings = this.settings$.value;
const existingIndex = this.findSettingsIndex(id, currentSettings);
if (existingIndex >= 0) {
this.updateExistingSettings(existingIndex, registeredSettings, currentSettings);
}
else {
this.addNewSettings(registeredSettings, currentSettings);
}
}
/**
* Retrieves all currently registered widget settings.
*
* @returns Array of all registered settings with their widget IDs
*/
getSettings() {
return this.settings$.value;
}
/**
* Removes all registered widget settings.
* This will cause the consolidated settings to return to default values.
*/
resetSettings() {
this.settings$.next([]);
}
/**
* Unregisters settings for a specific widget.
*
* @param id - Unique identifier of the widget to unregister
*
* @example
* ```typescript
* globalContextService.unregister('my-widget');
* ```
*/
unregister(id) {
if (!id)
return; // Silently ignore invalid IDs
const currentSettings = this.settings$.value;
const filteredSettings = currentSettings.filter(s => s.id !== id);
if (filteredSettings.length !== currentSettings.length) {
this.settings$.next(filteredSettings);
}
}
/**
* Returns an observable of consolidated global context settings.
*
* Settings are consolidated using OR logic - if any widget requires a setting to be shown,
* it will be included in the consolidated result. The observable emits distinct values only
* and is shared among all subscribers for optimal performance.
*
* @returns Observable that emits consolidated settings whenever they change
*
* @example
* ```typescript
* globalContextService.getConsolidatedSettings().subscribe(settings => {
* if (settings.showTimeContext) {
* // Show time context controls
* }
* });
* ```
*/
getConsolidatedSettings() {
return this.settings$.pipe(map(settingsArray => this.consolidateSettings(settingsArray)), distinctUntilChanged(this.areSettingsEqual), shareReplay(1));
}
/**
* Returns the default global context settings.
* These settings are used when no widgets have registered any settings.
*
* @returns Default settings object with all options disabled
*/
getDefaultSettings() {
return { ...DEFAULT_GLOBAL_CONTEXT_SETTINGS };
}
/**
* Registers a loading state for a specific widget.
* This will cause the `isLoading` computed signal to return true.
*
* @param id - Unique identifier of the widget in loading state
*
* @example
* ```typescript
* globalContextService.registerLoading('my-widget');
* console.log(globalContextService.isLoading()); // true
* ```
*/
registerLoading(id) {
if (!id)
return; // Silently ignore invalid IDs
this.loadingIds.update(ids => {
const newIds = new Set(ids);
newIds.add(id);
return newIds;
});
}
/**
* Unregisters a loading state for a specific widget.
* When no widgets are in loading state, `isLoading` will return false.
*
* @param id - Unique identifier of the widget to remove from loading state
*
* @example
* ```typescript
* globalContextService.unregisterLoading('my-widget');
* ```
*/
unregisterLoading(id) {
if (!id)
return; // Silently ignore invalid IDs
this.loadingIds.update(ids => {
const newIds = new Set(ids);
newIds.delete(id);
return newIds;
});
}
/**
* Returns the default global context state.
* This state is used as the initial configuration for new global context instances.
*
* @returns Default global context state with live refresh enabled and 1-minute time range
*
* @example
* ```typescript
* const defaultState = globalContextService.getDefaultState();
* console.log(defaultState.refreshOption); // 'live'
* ```
*/
getDefaultState() {
return createDefaultGlobalContextState();
}
/**
* Validates that a widget ID is not empty or null.
*
* @param id - Widget ID to validate
* @throws {Error} When ID is invalid
*/
validateWidgetId(id) {
if (!id || id.trim().length === 0) {
throw new Error('Widget ID cannot be empty or null');
}
}
/**
* Finds the index of settings for a given widget ID.
*
* @param id - Widget ID to search for
* @param settings - Array of settings to search in
* @returns Index of the settings or -1 if not found
*/
findSettingsIndex(id, settings) {
return settings.findIndex(s => s.id === id);
}
/**
* Updates existing settings for a widget.
*
* @param index - Index of the settings to update
* @param newSettings - New settings to apply
* @param currentSettings - Current settings array
*/
updateExistingSettings(index, newSettings, currentSettings) {
const updatedSettings = [...currentSettings];
updatedSettings[index] = newSettings;
this.settings$.next(updatedSettings);
}
/**
* Adds new settings to the settings array.
*
* @param newSettings - Settings to add
* @param currentSettings - Current settings array
*/
addNewSettings(newSettings, currentSettings) {
this.settings$.next([...currentSettings, newSettings]);
}
/**
* Consolidates multiple widget settings using OR logic.
*
* @param settingsArray - Array of widget settings to consolidate
* @returns Consolidated settings object
*/
consolidateSettings(settingsArray) {
return settingsArray.reduce((consolidated, current) => ({
showTimeContext: consolidated.showTimeContext || !!current.showTimeContext,
showAggregation: consolidated.showAggregation || !!current.showAggregation,
showAutoRefresh: consolidated.showAutoRefresh || !!current.showAutoRefresh,
showRefresh: consolidated.showRefresh || !!current.showRefresh,
showRefreshInterval: consolidated.showRefreshInterval || !!current.showRefreshInterval
}), { ...DEFAULT_GLOBAL_CONTEXT_SETTINGS });
}
/**
* Compares two settings objects for deep equality.
*
* @param prev - Previous settings
* @param curr - Current settings
* @returns True if settings are equal, false otherwise
*/
areSettingsEqual(prev, curr) {
return (prev.showTimeContext === curr.showTimeContext &&
prev.showAggregation === curr.showAggregation &&
prev.showAutoRefresh === curr.showAutoRefresh &&
prev.showRefresh === curr.showRefresh &&
prev.showRefreshInterval === curr.showRefreshInterval);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Service responsible for building and managing form groups for global context configuration.
*/
class GlobalContextFormService {
constructor() {
this.fb = inject(FormBuilder);
}
/**
* Creates a FormGroup for global context configuration.
* @param config Optional existing configuration to initialize form values
* @param defaults Optional override values for specific controls
* @returns A fully configured FormGroup
*/
buildForm(config = {}, defaults, skipProp) {
// Get base default values
const defaultValues = this.getDefaultValues();
// Extract valid config values
const validConfigKeys = Object.keys(defaultValues);
const configValues = pick(config, validConfigKeys);
// Merge configurations with appropriate precedence
const mergedValues = structuredClone(this.mergeValues(defaultValues, configValues, defaults));
const filtered = omit(mergedValues, skipProp);
return skipProp ? this.fb.group(filtered) : this.fb.group(mergedValues);
}
/**
* Extracts form values into a configuration object
* @param form The form to extract values from
* @returns GlobalContextConfig object
*/
getConfigFromForm(form) {
if (!form)
return null;
// Get raw form values
const formValue = form.getRawValue();
return {
dateTimeContext: formValue.dateTimeContext,
aggregation: formValue.aggregation,
isAutoRefreshEnabled: formValue.isAutoRefreshEnabled,
// Always use fixed 5s interval, ignore any form value
refreshInterval: GLOBAL_CONTEXT_DEFAULTS.REFRESH_INTERVAL,
refreshOption: formValue.refreshOption
};
}
/**
* Gets the default values for the global context form
*/
getDefaultValues() {
return createDefaultGlobalContextState();
}
/**
* Merges default values with config and explicit overrides
*/
mergeValues(defaultValues, configValues, overrideValues) {
// First merge config values over defaults
const mergedBasicValues = {
...defaultValues,
...configValues
};
// Special handling for dateTimeContext
if (configValues.dateTimeContext) {
mergedBasicValues.dateTimeContext = {
...defaultValues.dateTimeContext,
...configValues.dateTimeContext
};
}
// Apply explicit overrides if present
if (overrideValues) {
const result = {
...mergedBasicValues,
...overrideValues
};
if (overrideValues.dateTimeContext) {
result.dateTimeContext = {
...mergedBasicValues.dateTimeContext,
...overrideValues.dateTimeContext
};
}
return result;
}
return mergedBasicValues;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextFormService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextFormService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextFormService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Configuration constants for the event service.
*/
const EVENT_CONFIG = {
/** Name of the custom event dispatched on the window object */
EVENT_NAME: 'global-context',
/** Maximum number of cached event subjects to prevent memory leaks */
MAX_CACHED_SUBJECTS: 50
};
/**
* GlobalContextEventService manages global context events using the browser's native event system.
*
* This service provides a type-safe wrapper around window custom events for global context communication.
* It maintains event subjects for efficient observable-based event handling while ensuring proper
* cleanup and memory management.
*
* Key features:
* - Type-safe event emission and subscription
* - Latest value caching for each event type
* - Memory-efficient subject management
* - Integration with browser's native event system
*
* @example
* ```typescript
* // Emit an event
* eventService.emit('GLOBAL_CONTEXT_STATE_CHANGE', {
* refreshOption: 'live',
* isAutoRefreshEnabled: true
* });
*
* // Subscribe to events
* eventService.on('GLOBAL_CONTEXT_STATE_CHANGE').subscribe(state => {
* console.log('State changed:', state);
* });
*
* // Get the latest value
* const latestState = eventService.getLatestValue('GLOBAL_CONTEXT_STATE_CHANGE');
* ```
*/
class GlobalContextEventService {
constructor() {
/** Map storing BehaviorSubjects for each event type to cache latest values */
this.eventSubjects = new Map();
/** Observable stream of all global context events from the window */
this.events$ = fromEvent(window, EVENT_CONFIG.EVENT_NAME).pipe(map(event => event.detail));
}
/**
* Emits a global context event with type-safe payload.
*
* The event is both stored locally for latest value retrieval and dispatched
* as a browser custom event for cross-component communication.
*
* @param type - The specific event type to emit (must be a valid GlobalContextEventType)
* @param payload - Type-safe payload matching the event type requirements
*
* @throws {Error} When event type is invalid or payload validation fails
*
* @example
* ```typescript
* // Emit a state change event
* eventService.emit('GLOBAL_CONTEXT_STATE_CHANGE', {
* refreshOption: 'live',
* isAutoRefreshEnabled: true
* });
*
* // Emit a refresh event
* eventService.emit('GLOBAL_CONTEXT_REFRESH', {
* dateFrom: '2024-01-01',
* dateTo: '2024-01-02'
* });
* ```
*/
emit(type, payload) {
const payloadCopy = structuredClone(payload);
try {
this.validateEventType(type);
// Update the local subject with the new value
const subject = this.getOrCreateEventSubject(type);
subject.next(payloadCopy ?? null);
// Dispatch the browser event for cross-component communication
this.dispatchBrowserEvent(type, payloadCopy);
}
catch (error) {
console.error(`Failed to emit global context event '${type}':`, error);
throw error;
}
}
/**
* Creates an observable that emits whenever a specific event type occurs.
*
* The observable filters the global event stream to only emit events of the specified type.
* This provides efficient, reactive event handling without manual event listener management.
*
* @param eventType - The specific event type to observe
* @returns Observable that emits payloads of the specified event type
*
* @throws {Error} When event type is invalid
*
* @example
* ```typescript
* // Subscribe to state changes
* eventService.on('GLOBAL_CONTEXT_STATE_CHANGE')
* .pipe(takeUntilDestroyed())
* .subscribe(state => {
* console.log('New state:', state);
* });
*
* // Subscribe to refresh events with filtering
* eventService.on('GLOBAL_CONTEXT_REFRESH')
* .pipe(
* filter(data => data?.interval === 'MINUTES'),
* takeUntilDestroyed()
* )
* .subscribe(refreshData => {
* console.log('Minute-based refresh:', refreshData);
* });
* ```
*/
on(eventType) {
try {
this.validateEventType(eventType);
return this.events$.pipe(filter((event) => event.type === eventType), map(event => event.payload));
}
catch (error) {
console.error(`Failed to create observable for event '${eventType}':`, error);
throw error;
}
}
/**
* Retrieves the most recently emitted value for a specific event type.
*
* This method provides synchronous access to the latest event data without
* requiring a subscription. Returns null if no event of the specified type
* has been emitted yet.
*
* @param eventType - The event type to retrieve the latest value for
* @returns The latest emitted value or null if none exists
*
* @throws {Error} When event type is invalid
*
* @example
* ```typescript
* // Get the current state
* const currentState = eventService.getLatestValue('GLOBAL_CONTEXT_STATE_CHANGE');
* if (currentState) {
* console.log('Current refresh option:', currentState.refreshOption);
* }
*
* // Check for latest refresh data
* const refreshData = eventService.getLatestValue('GLOBAL_CONTEXT_REFRESH');
* if (refreshData?.dateFrom) {
* console.log('Last refresh from:', refreshData.dateFrom);
* }
* ```
*/
getLatestValue(eventType) {
try {
this.validateEventType(eventType);
const value = this.getOrCreateEventSubject(eventType).getValue();
return structuredClone(value);
}
catch (error) {
console.error(`Failed to get latest value for event '${eventType}':`, error);
throw error;
}
}
/**
* Synchronizes dateTimeContext from REFRESH event to STATE_CHANGE event.
*
* This method ensures that when a REFRESH event contains dateTimeContext data,
* it updates the STATE_CHANGE event subject's value directly without emitting a new event.
* This keeps the internal state synchronized without triggering listeners.
*
* @param refreshDateTimeContext - The dateTimeContext from the REFRESH event to sync
*
* @example
* ```typescript
* // When a refresh occurs with new dateTimeContext
* const refreshData = { dateFrom: '2024-01-01', dateTo: '2024-01-02', interval: 'HOURLY' };
* eventService.emit(GLOBAL_CONTEXT_EVENTS.REFRESH, refreshData);
*
* // Sync it to STATE_CHANGE internal state
* eventService.syncRefreshToStateChange(refreshData);
* ```
*/
syncRefreshToStateChange(refreshDateTimeContext) {
if (!refreshDateTimeContext) {
return;
}
// Only update if we have all required properties for DateTimeContext
if (refreshDateTimeContext.dateFrom &&
refreshDateTimeContext.dateTo &&
refreshDateTimeContext.interval) {
// Get the STATE_CHANGE subject
const stateChangeSubject = this.getOrCreateEventSubject(GLOBAL_CONTEXT_EVENTS.STATE_CHANGE);
const currentState = stateChangeSubject.getValue() || {};
// Merge refresh dateTimeContext into current state
const updatedState = {
...currentState,
dateTimeContext: {
dateFrom: refreshDateTimeContext.dateFrom,
dateTo: refreshDateTimeContext.dateTo,
interval: refreshDateTimeContext.interval
}
};
// Update the subject's value directly without emitting an event
stateChangeSubject.next(updatedState);
}
}
/**
* Validates that an event type is not empty or invalid.
*
* @param eventType - The event type to validate
* @throws {Error} When event type is invalid
*/
validateEventType(eventType) {
if (!eventType || typeof eventType !== 'string' || eventType.trim().length === 0) {
throw new Error('Event type must be a non-empty string');
}
}
/**
* Dispatches a browser custom event for cross-component communication.
*
* @param type - Event type
* @param payload - Event payload
*/
dispatchBrowserEvent(type, payload) {
const customEvent = new CustomEvent(EVENT_CONFIG.EVENT_NAME, {
detail: {
type,
payload,
timestamp: Date.now()
}
});
window.dispatchEvent(customEvent);
}
/**
* Gets or creates a BehaviorSubject for a specific event type.
* Implements memory management by limiting the number of cached subjects.
*
* @param type - The event type
* @returns BehaviorSubject for the event type
*/
getOrCreateEventSubject(type) {
if (!this.eventSubjects.has(type)) {
// Implement basic memory management
this.ensureSubjectCacheLimit();
const newSubject = new BehaviorSubject(null);
this.eventSubjects.set(type, newSubject);
}
return this.eventSubjects.get(type);
}
/**
* Ensures the event subject cache doesn't exceed the maximum limit.
* Removes the oldest subjects when limit is reached.
*/
ensureSubjectCacheLimit() {
if (this.eventSubjects.size >= EVENT_CONFIG.MAX_CACHED_SUBJECTS) {
const firstKey = this.eventSubjects.keys().next().value;
if (firstKey) {
const subject = this.eventSubjects.get(firstKey);
if (subject && !subject.closed) {
subject.complete();
}
this.eventSubjects.delete(firstKey);
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextEventService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GlobalContextEventService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Service responsible for managing widget-level control settings and their interaction
* with dashboard-level controls. Handles the resolution of settings based on display mode,
* link states, and refresh options.
*
* Key responsibilities:
* - Widget settings resolution for different display modes
* - Link state management between inline and dashboard controls
* - Dashboard control calculations based on link states
* - Settings toggle operations for individual and bulk link changes
*/
class WidgetControlService {
// ========================================
// PUBLIC API METHODS
// ========================================
/**
* Resolves widget settings for inline controls based on the specified mode and refresh option.
* For dashboard mode, calculates the split between inline and dashboard controls.
*
* @param controls - Widget control configuration containing settings and links
* @param mode - Display mode determining which settings to use
* @param refreshOption - Refresh mode (live/history) for settings selection
* @returns Resolved settings with inline controls, links, and dashboard controls
*/
resolveInlineControlSettings(controls, mode, refreshOption) {
const baseSettings = this.getInlineSettingsFromWidgetControls(controls, mode, refreshOption);
const baseLinks = this.getDefaultLinksFromControls(controls, mode, refreshOption);
if (mode === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) {
// Calculate dashboard controls with the merged settings and links
const { newSettings: newInlineSettings, dashboardSettings } = this.calculateDashboardSettings(baseSettings, baseLinks);
return {
settings: newInlineSettings,
links: baseLinks,
dashboardControls: dashboardSettings
};
}
return {
settings: baseSettings,
links: baseLinks,
dashboardControls: {}
};
}
/**
* Resolves widget settings for configuration controls based on the specified mode and refresh option.
* Configuration controls are simpler and don't require dashboard control calculations.
*
* @param controls - Widget control configuration containing config settings
* @param mode - Display mode determining which config settings to use
* @param refreshOption - Refresh mode (live/history) for settings selection
* @returns Resolved configuration settings with empty links and dashboard controls
*/
resolveConfigControlSettings(controls, mode, refreshOption) {
const baseSettings = this.getConfigSettingsFromWidgetControls(controls, mode, refreshOption);
return {
settings: baseSettings,
links: {},
dashboardControls: {}
};
}
/**
* Extracts inline settings from widget controls for the specified display mode and refresh option.
* Applies fallback logic for invalid refresh options, defaulting to LIVE mode.
*
* @param controls - Widget control configuration
* @param displayMode - Display mode (dashboard, config, view_and_config)
* @param refreshOption - Refresh mode, may be invalid and will fallback to LIVE
* @returns Inline settings for the specified mode and refresh option
*/
getInlineSettingsFromWidgetControls(controls, displayMode, refreshOption) {
const validatedRefresh = this.validateRefreshOption(refreshOption);
const templateSettings = controls?.settings?.[displayMode]?.[validatedRefresh] || {};
return { ...templateSettings };
}
/**
* Extracts configuration settings from widget controls for the specified display mode and refresh option.
* Applies fallback logic for invalid refresh optio