@meshmakers/octo-meshboard 3.4.1320 → 3.4.1340
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource as RuntimeEntitySelectDataSource$2, RuntimeEntityDialogDataSource as RuntimeEntityDialogDataSource$2, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, TENANT_ID_PROVIDER, AssetRepoService, JobManagementService } from '@meshmakers/octo-services';
|
|
2
2
|
export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, Directive, ElementRef, NgZone, makeEnvironmentProviders, provideAppInitializer, InjectionToken
|
|
4
|
+
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, effect, untracked, Directive, ElementRef, NgZone, makeEnvironmentProviders, provideAppInitializer, InjectionToken } from '@angular/core';
|
|
5
5
|
import * as i1$1 from '@angular/forms';
|
|
6
6
|
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
7
7
|
import { firstValueFrom, from, map, Observable, of, catchError, interval, filter } from 'rxjs';
|
|
@@ -26,6 +26,7 @@ import { LoaderModule } from '@progress/kendo-angular-indicators';
|
|
|
26
26
|
import * as i1$4 from '@progress/kendo-angular-icons';
|
|
27
27
|
import { SVGIconModule } from '@progress/kendo-angular-icons';
|
|
28
28
|
import { arrowUpIcon, arrowDownIcon, minusIcon, arrowRightIcon, arrowLeftIcon, linkIcon, chevronDownIcon, chevronRightIcon, circleIcon, questionCircleIcon, minusCircleIcon, warningTriangleIcon, exclamationCircleIcon, xCircleIcon, checkCircleIcon, columnsIcon, sortAscIcon, filterIcon, searchIcon, chartPieIcon, infoCircleIcon, plusIcon, trashIcon, pencilIcon, chartLineIcon, gearsIcon, clipboardMarkdownIcon, copyIcon, gridIcon, heartIcon, gridLayoutIcon, chartLineMarkersIcon, chartColumnStackedIcon, chartDoughnutIcon, tableIcon, shareIcon, fileTxtIcon, checkIcon, xIcon, downloadIcon, uploadIcon, gearIcon, saveIcon, arrowRotateCwIcon, undoIcon } from '@progress/kendo-svg-icons';
|
|
29
|
+
import { ExpressionEvaluatorService, GetProcessDiagramDtoGQL, GetProcessDiagramsDtoGQL, CreateProcessDiagramDtoGQL, UpdateProcessDiagramDtoGQL, SymbolLibraryService, renderAnimations, getFlowParticlesAnimation, renderFlowParticles } from '@meshmakers/octo-process-diagrams';
|
|
29
30
|
import { NotificationService } from '@progress/kendo-angular-notification';
|
|
30
31
|
import * as i1$6 from '@progress/kendo-angular-gauges';
|
|
31
32
|
import { CollectionChangesService, KENDO_GAUGES } from '@progress/kendo-angular-gauges';
|
|
@@ -38,7 +39,6 @@ import { MarkdownModule } from 'ngx-markdown';
|
|
|
38
39
|
import * as i4$1 from '@progress/kendo-angular-label';
|
|
39
40
|
import { LabelModule, KENDO_LABELS } from '@progress/kendo-angular-label';
|
|
40
41
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
41
|
-
import { GetProcessDiagramDtoGQL, GetProcessDiagramsDtoGQL, CreateProcessDiagramDtoGQL, UpdateProcessDiagramDtoGQL, SymbolLibraryService, ExpressionEvaluatorService, renderAnimations, getFlowParticlesAnimation, renderFlowParticles } from '@meshmakers/octo-process-diagrams';
|
|
42
42
|
import * as i5 from '@progress/kendo-angular-layout';
|
|
43
43
|
import { LayoutModule, TabStripModule, TileLayoutModule, TileLayoutComponent } from '@progress/kendo-angular-layout';
|
|
44
44
|
import * as i4$2 from '@progress/kendo-angular-dateinputs';
|
|
@@ -2019,6 +2019,13 @@ class MeshBoardStateService {
|
|
|
2019
2019
|
*/
|
|
2020
2020
|
_entitySelectorRtIds = signal({}, /* @ts-ignore */
|
|
2021
2021
|
...(ngDevMode ? [{ debugName: "_entitySelectorRtIds" }] : /* istanbul ignore next */ []));
|
|
2022
|
+
/**
|
|
2023
|
+
* Transient variables published by widgets (KPI output variables, AB#5364).
|
|
2024
|
+
* Kept apart from `_meshBoardConfig` so publishing triggers no persistence and
|
|
2025
|
+
* no dirty state. Cleared on every board switch.
|
|
2026
|
+
*/
|
|
2027
|
+
_widgetVariables = signal([], /* @ts-ignore */
|
|
2028
|
+
...(ngDevMode ? [{ debugName: "_widgetVariables" }] : /* istanbul ignore next */ []));
|
|
2022
2029
|
// Public computed signals
|
|
2023
2030
|
meshBoardConfig = computed(() => this._meshBoardConfig(), /* @ts-ignore */
|
|
2024
2031
|
...(ngDevMode ? [{ debugName: "meshBoardConfig" }] : /* istanbul ignore next */ []));
|
|
@@ -2120,6 +2127,7 @@ class MeshBoardStateService {
|
|
|
2120
2127
|
if (movedWidgets.length > 0) {
|
|
2121
2128
|
console.warn(`Resolved ${movedWidgets.length} overlapping widget(s)`);
|
|
2122
2129
|
}
|
|
2130
|
+
this._widgetVariables.set([]);
|
|
2123
2131
|
this._meshBoardConfig.set(config);
|
|
2124
2132
|
this._persistedMeshBoardId.set(result.meshBoard.rtId);
|
|
2125
2133
|
this._existingWidgetRtIds.set(result.widgets.map(w => w.rtId));
|
|
@@ -2425,11 +2433,55 @@ class MeshBoardStateService {
|
|
|
2425
2433
|
// Variable Management
|
|
2426
2434
|
// ============================================================================
|
|
2427
2435
|
/**
|
|
2428
|
-
* Gets all variables
|
|
2436
|
+
* Gets all variables: the MeshBoard configuration's variables plus the runtime
|
|
2437
|
+
* variables published by widgets. A configuration variable wins on a name clash.
|
|
2438
|
+
* Reads signals, so `computed()`s calling it react to both.
|
|
2429
2439
|
*/
|
|
2430
2440
|
getVariables() {
|
|
2441
|
+
const configVariables = this.configVariables();
|
|
2442
|
+
const widgetVariables = this._widgetVariables();
|
|
2443
|
+
if (widgetVariables.length === 0) {
|
|
2444
|
+
return configVariables;
|
|
2445
|
+
}
|
|
2446
|
+
const configNames = new Set(configVariables.map(v => v.name));
|
|
2447
|
+
return [...configVariables, ...widgetVariables.filter(v => !configNames.has(v.name))];
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Variables stored in the MeshBoard configuration (without widget variables).
|
|
2451
|
+
* Every write path builds on this, so widget variables never reach the config.
|
|
2452
|
+
*/
|
|
2453
|
+
configVariables() {
|
|
2431
2454
|
return this._meshBoardConfig().variables ?? [];
|
|
2432
2455
|
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Publishes a widget's value as runtime variable (source `'widget'`).
|
|
2458
|
+
* Replaces a variable the same widget published under another name, and does
|
|
2459
|
+
* nothing when name and value are unchanged, so publishing effects cannot loop.
|
|
2460
|
+
*/
|
|
2461
|
+
setWidgetVariable(widgetId, name, value) {
|
|
2462
|
+
const current = this._widgetVariables();
|
|
2463
|
+
const existing = current.find(v => v.widgetId === widgetId);
|
|
2464
|
+
if (existing && existing.name === name && existing.value === value) {
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
const variable = {
|
|
2468
|
+
name,
|
|
2469
|
+
type: value !== '' && !isNaN(Number(value)) ? 'number' : 'string',
|
|
2470
|
+
source: 'widget',
|
|
2471
|
+
value,
|
|
2472
|
+
widgetId
|
|
2473
|
+
};
|
|
2474
|
+
this._widgetVariables.set([...current.filter(v => v.widgetId !== widgetId), variable]);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Removes the runtime variable published by a widget (e.g. when it is destroyed).
|
|
2478
|
+
*/
|
|
2479
|
+
clearWidgetVariable(widgetId) {
|
|
2480
|
+
const current = this._widgetVariables();
|
|
2481
|
+
if (current.some(v => v.widgetId === widgetId)) {
|
|
2482
|
+
this._widgetVariables.set(current.filter(v => v.widgetId !== widgetId));
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2433
2485
|
/**
|
|
2434
2486
|
* Gets a specific variable by name.
|
|
2435
2487
|
*/
|
|
@@ -2449,7 +2501,7 @@ class MeshBoardStateService {
|
|
|
2449
2501
|
* Sets the value of a single variable.
|
|
2450
2502
|
*/
|
|
2451
2503
|
setVariableValue(name, value) {
|
|
2452
|
-
const variables = [...this.
|
|
2504
|
+
const variables = [...this.configVariables()];
|
|
2453
2505
|
const index = variables.findIndex(v => v.name === name);
|
|
2454
2506
|
if (index >= 0) {
|
|
2455
2507
|
variables[index] = { ...variables[index], value };
|
|
@@ -2460,14 +2512,14 @@ class MeshBoardStateService {
|
|
|
2460
2512
|
* Adds a new variable to the MeshBoard.
|
|
2461
2513
|
*/
|
|
2462
2514
|
addVariable(variable) {
|
|
2463
|
-
const variables = [...this.
|
|
2515
|
+
const variables = [...this.configVariables(), variable];
|
|
2464
2516
|
this.updateVariables(variables);
|
|
2465
2517
|
}
|
|
2466
2518
|
/**
|
|
2467
2519
|
* Removes a variable by name.
|
|
2468
2520
|
*/
|
|
2469
2521
|
removeVariable(name) {
|
|
2470
|
-
const variables = this.
|
|
2522
|
+
const variables = this.configVariables().filter(v => v.name !== name);
|
|
2471
2523
|
this.updateVariables(variables);
|
|
2472
2524
|
}
|
|
2473
2525
|
// ============================================================================
|
|
@@ -2620,7 +2672,7 @@ class MeshBoardStateService {
|
|
|
2620
2672
|
*/
|
|
2621
2673
|
setTimeFilterVariables(fromISO, toISO) {
|
|
2622
2674
|
// Filter out existing timeFilter variables
|
|
2623
|
-
const currentVars = this.
|
|
2675
|
+
const currentVars = this.configVariables().filter(v => v.source !== 'timeFilter');
|
|
2624
2676
|
// Create new time filter variables
|
|
2625
2677
|
const timeFilterVars = [
|
|
2626
2678
|
{
|
|
@@ -2643,7 +2695,7 @@ class MeshBoardStateService {
|
|
|
2643
2695
|
* Clears time filter variables (called when time filter is disabled).
|
|
2644
2696
|
*/
|
|
2645
2697
|
clearTimeFilterVariables() {
|
|
2646
|
-
const currentVars = this.
|
|
2698
|
+
const currentVars = this.configVariables().filter(v => v.source !== 'timeFilter');
|
|
2647
2699
|
this.updateVariables(currentVars);
|
|
2648
2700
|
}
|
|
2649
2701
|
// ============================================================================
|
|
@@ -2687,7 +2739,7 @@ class MeshBoardStateService {
|
|
|
2687
2739
|
*/
|
|
2688
2740
|
setEntitySelectorVariables(selectorId, values) {
|
|
2689
2741
|
// Remove existing variables for this selector
|
|
2690
|
-
const currentVars = this.
|
|
2742
|
+
const currentVars = this.configVariables().filter(v => !(v.source === 'entitySelector' && v.entitySelectorId === selectorId));
|
|
2691
2743
|
// Create new variables
|
|
2692
2744
|
const selectorVars = values.map(v => ({
|
|
2693
2745
|
name: v.name,
|
|
@@ -2702,7 +2754,7 @@ class MeshBoardStateService {
|
|
|
2702
2754
|
* Clears entity selector variables for a given selector.
|
|
2703
2755
|
*/
|
|
2704
2756
|
clearEntitySelectorVariables(selectorId) {
|
|
2705
|
-
const currentVars = this.
|
|
2757
|
+
const currentVars = this.configVariables().filter(v => !(v.source === 'entitySelector' && v.entitySelectorId === selectorId));
|
|
2706
2758
|
this.updateVariables(currentVars);
|
|
2707
2759
|
}
|
|
2708
2760
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -5360,11 +5412,162 @@ function applyValueMultiplier(value, multiplier) {
|
|
|
5360
5412
|
return value * multiplier;
|
|
5361
5413
|
}
|
|
5362
5414
|
|
|
5415
|
+
const VARIABLE_REFERENCE = /\$\{([^}]+)\}|\$([a-zA-Z_]\w*)/g;
|
|
5416
|
+
const IDENTIFIER_PREFIX = '__v_';
|
|
5417
|
+
const NUMERIC_VALUE = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
|
|
5418
|
+
/**
|
|
5419
|
+
* Safe expr-eval identifier for a variable name. Every character outside `[A-Za-z0-9]`,
|
|
5420
|
+
* including `_`, is hex-escaped as `_<hex>_`, so underscores only ever delimit escapes and
|
|
5421
|
+
* the mapping is injective (`my-var` and `my_2d_var` stay distinct).
|
|
5422
|
+
*/
|
|
5423
|
+
function toIdentifier(name) {
|
|
5424
|
+
return IDENTIFIER_PREFIX + name.replace(/[^A-Za-z0-9]/g, c => `_${c.charCodeAt(0).toString(16)}_`);
|
|
5425
|
+
}
|
|
5426
|
+
/**
|
|
5427
|
+
* Replaces `${name}` / `$name` references with safe identifiers.
|
|
5428
|
+
*/
|
|
5429
|
+
function rewriteFormula(formula) {
|
|
5430
|
+
const referencedNames = [];
|
|
5431
|
+
const expression = (formula ?? '').replace(VARIABLE_REFERENCE, (_match, bracketName, simpleName) => {
|
|
5432
|
+
const name = (bracketName ?? simpleName).trim();
|
|
5433
|
+
if (!referencedNames.includes(name)) {
|
|
5434
|
+
referencedNames.push(name);
|
|
5435
|
+
}
|
|
5436
|
+
return toIdentifier(name);
|
|
5437
|
+
});
|
|
5438
|
+
return { expression, referencedNames };
|
|
5439
|
+
}
|
|
5440
|
+
/**
|
|
5441
|
+
* Validates a formula: syntax, and that every referenced variable exists.
|
|
5442
|
+
* A bare identifier (`a` instead of `${a}`) is reported as unknown variable.
|
|
5443
|
+
*/
|
|
5444
|
+
function validateFormula(formula, availableNames, evaluator) {
|
|
5445
|
+
if (!formula?.trim()) {
|
|
5446
|
+
return { valid: false, error: 'Formula is empty', unknownVariables: [] };
|
|
5447
|
+
}
|
|
5448
|
+
const { expression, referencedNames } = rewriteFormula(formula);
|
|
5449
|
+
const syntax = evaluator.validate(expression);
|
|
5450
|
+
if (!syntax.valid) {
|
|
5451
|
+
return { valid: false, error: syntax.error ?? 'Invalid formula', unknownVariables: [] };
|
|
5452
|
+
}
|
|
5453
|
+
const unknownVariables = referencedNames.filter(name => !availableNames.includes(name));
|
|
5454
|
+
const bareIdentifiers = evaluator.getVariables(expression)
|
|
5455
|
+
.filter(identifier => !identifier.startsWith(IDENTIFIER_PREFIX));
|
|
5456
|
+
for (const identifier of bareIdentifiers) {
|
|
5457
|
+
if (!unknownVariables.includes(identifier)) {
|
|
5458
|
+
unknownVariables.push(identifier);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
if (unknownVariables.length > 0) {
|
|
5462
|
+
return {
|
|
5463
|
+
valid: false,
|
|
5464
|
+
error: `Unknown variable${unknownVariables.length > 1 ? 's' : ''}: ${unknownVariables.join(', ')}`,
|
|
5465
|
+
unknownVariables
|
|
5466
|
+
};
|
|
5467
|
+
}
|
|
5468
|
+
return { valid: true, unknownVariables: [] };
|
|
5469
|
+
}
|
|
5470
|
+
/** Converts a variable's string value to the type expr-eval should see. */
|
|
5471
|
+
function toContextValue(value) {
|
|
5472
|
+
const trimmed = value.trim();
|
|
5473
|
+
if (NUMERIC_VALUE.test(trimmed)) {
|
|
5474
|
+
return Number(trimmed);
|
|
5475
|
+
}
|
|
5476
|
+
if (trimmed === 'true' || trimmed === 'false') {
|
|
5477
|
+
return trimmed === 'true';
|
|
5478
|
+
}
|
|
5479
|
+
return value;
|
|
5480
|
+
}
|
|
5481
|
+
/**
|
|
5482
|
+
* Evaluates a formula against the given variables.
|
|
5483
|
+
*
|
|
5484
|
+
* Returns `pending` while the formula is empty or a referenced variable is missing or empty
|
|
5485
|
+
* (e.g. an upstream widget has not published yet), so the widget shows `-` instead of an error.
|
|
5486
|
+
*/
|
|
5487
|
+
function evaluateFormula(formula, variables, evaluator) {
|
|
5488
|
+
if (!formula?.trim()) {
|
|
5489
|
+
return { status: 'pending' };
|
|
5490
|
+
}
|
|
5491
|
+
const { expression, referencedNames } = rewriteFormula(formula);
|
|
5492
|
+
const context = {};
|
|
5493
|
+
for (const name of referencedNames) {
|
|
5494
|
+
const variable = variables.find(v => v.name === name);
|
|
5495
|
+
const value = variable?.value || variable?.defaultValue;
|
|
5496
|
+
if (value === undefined || value === '') {
|
|
5497
|
+
return { status: 'pending' };
|
|
5498
|
+
}
|
|
5499
|
+
context[toIdentifier(name)] = toContextValue(value);
|
|
5500
|
+
}
|
|
5501
|
+
const result = evaluator.evaluate(expression, context);
|
|
5502
|
+
if (!result.success) {
|
|
5503
|
+
return { status: 'error', error: result.error ?? 'Evaluation failed' };
|
|
5504
|
+
}
|
|
5505
|
+
const value = result.value;
|
|
5506
|
+
if (typeof value === 'number') {
|
|
5507
|
+
return Number.isFinite(value)
|
|
5508
|
+
? { status: 'ok', value }
|
|
5509
|
+
: { status: 'error', error: 'Result is not a finite number' };
|
|
5510
|
+
}
|
|
5511
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
5512
|
+
return { status: 'ok', value };
|
|
5513
|
+
}
|
|
5514
|
+
return { status: 'error', error: 'Result is not a number' };
|
|
5515
|
+
}
|
|
5516
|
+
/** True when the widget is a KPI in formula mode. */
|
|
5517
|
+
function isFormulaKpi(widget) {
|
|
5518
|
+
return widget.type === 'kpi' && widget.valueMode === 'formula';
|
|
5519
|
+
}
|
|
5520
|
+
/**
|
|
5521
|
+
* Finds widgets that depend on their own output, directly or through other widgets.
|
|
5522
|
+
*
|
|
5523
|
+
* Graph: a formula KPI depends on every widget whose `outputVariableName` its formula
|
|
5524
|
+
* references. A name that is also a configured MeshBoard variable resolves to that variable
|
|
5525
|
+
* at runtime (config variables win), so it creates no dependency on the widget.
|
|
5526
|
+
* Returns the IDs of all widgets that are part of a cycle (including self-references).
|
|
5527
|
+
*/
|
|
5528
|
+
function findVariableCycles(widgets, configuredVariableNames = []) {
|
|
5529
|
+
const configuredNames = new Set(configuredVariableNames);
|
|
5530
|
+
const producerByName = new Map();
|
|
5531
|
+
for (const widget of widgets) {
|
|
5532
|
+
const name = widget.type === 'kpi' ? widget.outputVariableName?.trim() : undefined;
|
|
5533
|
+
if (name) {
|
|
5534
|
+
producerByName.set(name, widget.id);
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
const dependencies = new Map();
|
|
5538
|
+
for (const widget of widgets) {
|
|
5539
|
+
if (!isFormulaKpi(widget))
|
|
5540
|
+
continue;
|
|
5541
|
+
const producers = rewriteFormula(widget.formula ?? '').referencedNames
|
|
5542
|
+
.map(name => configuredNames.has(name) ? undefined : producerByName.get(name))
|
|
5543
|
+
.filter((id) => id !== undefined);
|
|
5544
|
+
dependencies.set(widget.id, producers);
|
|
5545
|
+
}
|
|
5546
|
+
const inCycle = new Set();
|
|
5547
|
+
for (const start of dependencies.keys()) {
|
|
5548
|
+
const visited = new Set();
|
|
5549
|
+
const stack = [...(dependencies.get(start) ?? [])];
|
|
5550
|
+
while (stack.length > 0) {
|
|
5551
|
+
const current = stack.pop();
|
|
5552
|
+
if (current === start) {
|
|
5553
|
+
inCycle.add(start);
|
|
5554
|
+
break;
|
|
5555
|
+
}
|
|
5556
|
+
if (visited.has(current))
|
|
5557
|
+
continue;
|
|
5558
|
+
visited.add(current);
|
|
5559
|
+
stack.push(...(dependencies.get(current) ?? []));
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
return inCycle;
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5363
5565
|
class KpiWidgetComponent {
|
|
5364
5566
|
dataService = inject(DashboardDataService);
|
|
5365
5567
|
queryExecutor = inject(QueryExecutorService);
|
|
5366
5568
|
stateService = inject(MeshBoardStateService);
|
|
5367
5569
|
variableService = inject(MeshBoardVariableService);
|
|
5570
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
5368
5571
|
/**
|
|
5369
5572
|
* Row __typenames KPI extraction recognises.
|
|
5370
5573
|
* Runtime queries discriminate; stream-data queries collapse all kinds
|
|
@@ -5389,6 +5592,34 @@ class KpiWidgetComponent {
|
|
|
5389
5592
|
isLoading = this._isLoading.asReadonly();
|
|
5390
5593
|
data = this._data.asReadonly();
|
|
5391
5594
|
error = this._error.asReadonly();
|
|
5595
|
+
/**
|
|
5596
|
+
* Signal mirror of the `config` input, so computeds re-run when the config
|
|
5597
|
+
* changes (the input itself is a plain property). Falls back to the input
|
|
5598
|
+
* before the first lifecycle hook.
|
|
5599
|
+
*/
|
|
5600
|
+
_config = signal(undefined, /* @ts-ignore */
|
|
5601
|
+
...(ngDevMode ? [{ debugName: "_config" }] : /* istanbul ignore next */ []));
|
|
5602
|
+
currentConfig() {
|
|
5603
|
+
return this._config() ?? this.config;
|
|
5604
|
+
}
|
|
5605
|
+
constructor() {
|
|
5606
|
+
// Publishes the raw value as runtime MeshBoard variable (AB#5364).
|
|
5607
|
+
effect(() => {
|
|
5608
|
+
const config = this.currentConfig();
|
|
5609
|
+
if (!config)
|
|
5610
|
+
return;
|
|
5611
|
+
const name = config.outputVariableName?.trim();
|
|
5612
|
+
const value = name ? this.publishedValue() : null;
|
|
5613
|
+
untracked(() => {
|
|
5614
|
+
if (name && value !== null) {
|
|
5615
|
+
this.stateService.setWidgetVariable(config.id, name, value);
|
|
5616
|
+
}
|
|
5617
|
+
else {
|
|
5618
|
+
this.stateService.clearWidgetVariable(config.id);
|
|
5619
|
+
}
|
|
5620
|
+
});
|
|
5621
|
+
});
|
|
5622
|
+
}
|
|
5392
5623
|
/**
|
|
5393
5624
|
* Check if widget is not configured (needs data source setup).
|
|
5394
5625
|
* This is a method (not computed) to ensure it re-evaluates when config changes via @Input.
|
|
@@ -5408,39 +5639,92 @@ class KpiWidgetComponent {
|
|
|
5408
5639
|
return !dataSource.queryRtId;
|
|
5409
5640
|
}
|
|
5410
5641
|
if (dataSource.type === 'static') {
|
|
5642
|
+
if (this.isFormulaMode()) {
|
|
5643
|
+
return !this.config.formula?.trim();
|
|
5644
|
+
}
|
|
5411
5645
|
return false;
|
|
5412
5646
|
}
|
|
5413
5647
|
return false;
|
|
5414
5648
|
}
|
|
5415
|
-
value
|
|
5649
|
+
/** True when the value is computed from a formula (AB#5364). */
|
|
5650
|
+
isFormulaMode() {
|
|
5651
|
+
return this.currentConfig()?.valueMode === 'formula';
|
|
5652
|
+
}
|
|
5653
|
+
/**
|
|
5654
|
+
* Formula evaluation result, live over the MeshBoard variables.
|
|
5655
|
+
* `null` outside formula mode. A widget in a circular reference is not evaluated,
|
|
5656
|
+
* otherwise publishing would ping-pong between the widgets of the cycle.
|
|
5657
|
+
*/
|
|
5658
|
+
formulaResult = computed(() => {
|
|
5659
|
+
const config = this.currentConfig();
|
|
5660
|
+
if (config?.valueMode !== 'formula')
|
|
5661
|
+
return null;
|
|
5662
|
+
const configuredNames = (this.stateService.meshBoardConfig().variables ?? []).map(v => v.name);
|
|
5663
|
+
if (findVariableCycles(this.stateService.widgets(), configuredNames).has(config.id)) {
|
|
5664
|
+
return { status: 'error', error: 'Circular variable reference' };
|
|
5665
|
+
}
|
|
5666
|
+
return evaluateFormula(config.formula, this.stateService.getVariables(), this.expressionEvaluator);
|
|
5667
|
+
}, /* @ts-ignore */
|
|
5668
|
+
...(ngDevMode ? [{ debugName: "formulaResult" }] : /* istanbul ignore next */ []));
|
|
5669
|
+
/** Error message of the formula (syntax, evaluation or circular reference). */
|
|
5670
|
+
formulaError = computed(() => {
|
|
5671
|
+
const result = this.formulaResult();
|
|
5672
|
+
return result?.status === 'error' ? result.error : null;
|
|
5673
|
+
}, /* @ts-ignore */
|
|
5674
|
+
...(ngDevMode ? [{ debugName: "formulaError" }] : /* istanbul ignore next */ []));
|
|
5675
|
+
/**
|
|
5676
|
+
* Unformatted value: the formula result, or the value taken from the loaded data
|
|
5677
|
+
* (query values already scaled by `valueMultiplier`).
|
|
5678
|
+
*/
|
|
5679
|
+
rawValue = computed(() => {
|
|
5680
|
+
const formulaResult = this.formulaResult();
|
|
5681
|
+
if (formulaResult) {
|
|
5682
|
+
return formulaResult.status === 'ok' ? formulaResult.value : null;
|
|
5683
|
+
}
|
|
5416
5684
|
const data = this._data();
|
|
5417
5685
|
if (!data)
|
|
5418
|
-
return
|
|
5686
|
+
return null;
|
|
5687
|
+
const config = this.currentConfig();
|
|
5419
5688
|
// Determine attribute name based on data source type
|
|
5420
5689
|
let attributeName;
|
|
5421
|
-
if (
|
|
5690
|
+
if (config?.dataSource?.type === 'persistentQuery') {
|
|
5422
5691
|
attributeName = '_queryValue';
|
|
5423
5692
|
}
|
|
5424
|
-
else if (
|
|
5693
|
+
else if (config?.dataSource?.type === 'static') {
|
|
5425
5694
|
attributeName = '_staticValue';
|
|
5426
5695
|
}
|
|
5427
5696
|
else {
|
|
5428
|
-
attributeName =
|
|
5697
|
+
attributeName = config?.valueAttribute;
|
|
5429
5698
|
}
|
|
5430
5699
|
// First check for system properties (direct properties on RuntimeEntityData)
|
|
5431
5700
|
const systemValue = this.getSystemPropertyValue(data, attributeName);
|
|
5432
5701
|
if (systemValue !== undefined) {
|
|
5433
|
-
return
|
|
5702
|
+
return systemValue;
|
|
5434
5703
|
}
|
|
5435
5704
|
// Then check in the attributes array
|
|
5436
|
-
|
|
5437
|
-
return '-';
|
|
5438
|
-
const attr = data.attributes.find(a => a.attributeName === attributeName);
|
|
5439
|
-
if (!attr)
|
|
5440
|
-
return '-';
|
|
5441
|
-
return this.formatDisplayValue(attr.value);
|
|
5705
|
+
return data.attributes?.find(a => a.attributeName === attributeName)?.value ?? null;
|
|
5442
5706
|
}, /* @ts-ignore */
|
|
5707
|
+
...(ngDevMode ? [{ debugName: "rawValue" }] : /* istanbul ignore next */ []));
|
|
5708
|
+
value = computed(() => this.formatDisplayValue(this.rawValue()), /* @ts-ignore */
|
|
5443
5709
|
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
5710
|
+
/**
|
|
5711
|
+
* Value published as output variable: the raw value as string, or `null` while
|
|
5712
|
+
* there is none (nothing loaded, pending formula, unresolved placeholder, failed load).
|
|
5713
|
+
* During a refresh the last value stays published, so dependents do not flicker to `-`.
|
|
5714
|
+
*/
|
|
5715
|
+
publishedValue = computed(() => {
|
|
5716
|
+
if (this._error())
|
|
5717
|
+
return null;
|
|
5718
|
+
const raw = this.rawValue();
|
|
5719
|
+
if (raw === null || raw === undefined)
|
|
5720
|
+
return null;
|
|
5721
|
+
const str = String(raw);
|
|
5722
|
+
if (str === '' || str === 'null' || this.variableService.hasUnresolvedVariables(str)) {
|
|
5723
|
+
return null;
|
|
5724
|
+
}
|
|
5725
|
+
return str;
|
|
5726
|
+
}, /* @ts-ignore */
|
|
5727
|
+
...(ngDevMode ? [{ debugName: "publishedValue" }] : /* istanbul ignore next */ []));
|
|
5444
5728
|
/**
|
|
5445
5729
|
* Gets a system property value from RuntimeEntityData.
|
|
5446
5730
|
* System properties are top-level properties like rtId, ckTypeId, rtWellKnownName.
|
|
@@ -5530,18 +5814,32 @@ class KpiWidgetComponent {
|
|
|
5530
5814
|
}, /* @ts-ignore */
|
|
5531
5815
|
...(ngDevMode ? [{ debugName: "trendClass" }] : /* istanbul ignore next */ []));
|
|
5532
5816
|
comparisonText = computed(() => {
|
|
5533
|
-
|
|
5817
|
+
const comparisonText = this.currentConfig()?.comparisonText;
|
|
5818
|
+
if (!comparisonText)
|
|
5534
5819
|
return null;
|
|
5535
5820
|
const variables = this.stateService.getVariables();
|
|
5536
|
-
return this.variableService.resolveVariables(
|
|
5821
|
+
return this.variableService.resolveVariables(comparisonText, variables);
|
|
5537
5822
|
}, /* @ts-ignore */
|
|
5538
5823
|
...(ngDevMode ? [{ debugName: "comparisonText" }] : /* istanbul ignore next */ []));
|
|
5539
5824
|
ngOnInit() {
|
|
5825
|
+
this._config.set(this.config);
|
|
5540
5826
|
this.loadData();
|
|
5541
5827
|
}
|
|
5542
5828
|
ngOnChanges(changes) {
|
|
5543
|
-
if (changes['config']
|
|
5544
|
-
|
|
5829
|
+
if (changes['config']) {
|
|
5830
|
+
const previous = changes['config'].previousValue;
|
|
5831
|
+
if (previous && previous.id !== this.config?.id) {
|
|
5832
|
+
this.stateService.clearWidgetVariable(previous.id);
|
|
5833
|
+
}
|
|
5834
|
+
this._config.set(this.config);
|
|
5835
|
+
if (!changes['config'].firstChange) {
|
|
5836
|
+
this.loadData();
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
ngOnDestroy() {
|
|
5841
|
+
if (this.config?.id) {
|
|
5842
|
+
this.stateService.clearWidgetVariable(this.config.id);
|
|
5545
5843
|
}
|
|
5546
5844
|
}
|
|
5547
5845
|
refresh() {
|
|
@@ -5553,6 +5851,12 @@ class KpiWidgetComponent {
|
|
|
5553
5851
|
return;
|
|
5554
5852
|
}
|
|
5555
5853
|
const dataSource = this.config?.dataSource;
|
|
5854
|
+
if (this.isFormulaMode()) {
|
|
5855
|
+
// Formula values are computed live from the variables (see formulaResult)
|
|
5856
|
+
this._data.set(null);
|
|
5857
|
+
this._error.set(null);
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5556
5860
|
if (dataSource.type === 'static') {
|
|
5557
5861
|
// Resolve static value with variable substitution
|
|
5558
5862
|
const staticValue = this.config.staticValue ?? '';
|
|
@@ -5753,12 +6057,12 @@ class KpiWidgetComponent {
|
|
|
5753
6057
|
return this.variableService.convertToFieldFilterDto(filters, variables);
|
|
5754
6058
|
}
|
|
5755
6059
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5756
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiWidgetComponent, isStandalone: true, selector: "mm-kpi-widget", inputs: { config: "config" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"kpi-widget\" [class.loading]=\"isLoading()\" [class.error]=\"error()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>...</span>\n </div>\n } @else if (error()) {\n <div class=\"error-message\">\n <span>{{ error() }}</span>\n </div>\n } @else {\n <div class=\"kpi-content\">\n <div class=\"kpi-value-container\">\n @if (config.prefix) {\n <span class=\"kpi-prefix\">{{ config.prefix }}</span>\n }\n <span class=\"kpi-value\">{{ value() }}</span>\n @if (config.suffix) {\n <span class=\"kpi-suffix\">{{ config.suffix }}</span>\n }\n </div>\n\n @if (comparisonText()) {\n <div class=\"kpi-comparison\" [ngClass]=\"trendClass()\">\n @if (config.trend) {\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"xsmall\"></kendo-svg-icon>\n }\n {{ comparisonText() }}\n </div>\n }\n\n @if (label()) {\n <div class=\"kpi-label\">{{ label() }}</div>\n }\n </div>\n\n @if (config.trend) {\n <div class=\"kpi-trend\" [ngClass]=\"trendClass()\">\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"medium\"></kendo-svg-icon>\n </div>\n }\n }\n</div>\n", styles: [".kpi-widget{display:flex;align-items:center;justify-content:center;height:100%;padding:16px;gap:16px}.kpi-content{display:flex;flex-direction:column;align-items:center;text-align:center}.kpi-value-container{display:flex;align-items:baseline;gap:4px}.kpi-prefix,.kpi-suffix{font-size:1.25rem;font-weight:500;color:var(--kendo-color-subtle, #6c757d)}.kpi-value{font-size:2.5rem;font-weight:700;color:var(--kendo-color-on-surface, #212529);line-height:1}.kpi-comparison{margin-top:4px;font-size:.75rem;font-weight:500;display:flex;align-items:center;gap:4px;justify-content:center}.kpi-comparison.trend-up{color:var(--kendo-color-success, #28a745)}.kpi-comparison.trend-down{color:var(--kendo-color-error, #dc3545)}.kpi-comparison.trend-neutral{color:var(--kendo-color-subtle, #6c757d)}.kpi-label{margin-top:8px;font-size:.875rem;color:var(--kendo-color-subtle, #6c757d)}.kpi-trend{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%}.kpi-trend.trend-up{background:#28a74526;color:var(--kendo-color-success, #28a745)}.kpi-trend.trend-down{background:#dc354526;color:var(--kendo-color-error, #dc3545)}.kpi-trend.trend-neutral{background:#6c757d26;color:var(--kendo-color-subtle, #6c757d)}@media(max-width:768px){.kpi-value{font-size:1.75rem}.kpi-prefix,.kpi-suffix{font-size:1rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
6060
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiWidgetComponent, isStandalone: true, selector: "mm-kpi-widget", inputs: { config: "config" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"kpi-widget\" [class.loading]=\"isLoading()\" [class.error]=\"error() || formulaError()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>...</span>\n </div>\n } @else if (error() || formulaError()) {\n <div class=\"error-message\">\n <span>{{ error() ?? formulaError() }}</span>\n </div>\n } @else {\n <div class=\"kpi-content\">\n <div class=\"kpi-value-container\">\n @if (config.prefix) {\n <span class=\"kpi-prefix\">{{ config.prefix }}</span>\n }\n <span class=\"kpi-value\">{{ value() }}</span>\n @if (config.suffix) {\n <span class=\"kpi-suffix\">{{ config.suffix }}</span>\n }\n </div>\n\n @if (comparisonText()) {\n <div class=\"kpi-comparison\" [ngClass]=\"trendClass()\">\n @if (config.trend) {\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"xsmall\"></kendo-svg-icon>\n }\n {{ comparisonText() }}\n </div>\n }\n\n @if (label()) {\n <div class=\"kpi-label\">{{ label() }}</div>\n }\n </div>\n\n @if (config.trend) {\n <div class=\"kpi-trend\" [ngClass]=\"trendClass()\">\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"medium\"></kendo-svg-icon>\n </div>\n }\n }\n</div>\n", styles: [".kpi-widget{display:flex;align-items:center;justify-content:center;height:100%;padding:16px;gap:16px}.kpi-content{display:flex;flex-direction:column;align-items:center;text-align:center}.kpi-value-container{display:flex;align-items:baseline;gap:4px}.kpi-prefix,.kpi-suffix{font-size:1.25rem;font-weight:500;color:var(--kendo-color-subtle, #6c757d)}.kpi-value{font-size:2.5rem;font-weight:700;color:var(--kendo-color-on-surface, #212529);line-height:1}.kpi-comparison{margin-top:4px;font-size:.75rem;font-weight:500;display:flex;align-items:center;gap:4px;justify-content:center}.kpi-comparison.trend-up{color:var(--kendo-color-success, #28a745)}.kpi-comparison.trend-down{color:var(--kendo-color-error, #dc3545)}.kpi-comparison.trend-neutral{color:var(--kendo-color-subtle, #6c757d)}.kpi-label{margin-top:8px;font-size:.875rem;color:var(--kendo-color-subtle, #6c757d)}.kpi-trend{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%}.kpi-trend.trend-up{background:#28a74526;color:var(--kendo-color-success, #28a745)}.kpi-trend.trend-down{background:#dc354526;color:var(--kendo-color-error, #dc3545)}.kpi-trend.trend-neutral{background:#6c757d26;color:var(--kendo-color-subtle, #6c757d)}@media(max-width:768px){.kpi-value{font-size:1.75rem}.kpi-prefix,.kpi-suffix{font-size:1rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
5757
6061
|
}
|
|
5758
6062
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiWidgetComponent, decorators: [{
|
|
5759
6063
|
type: Component,
|
|
5760
|
-
args: [{ selector: 'mm-kpi-widget', standalone: true, imports: [CommonModule, SVGIconModule, WidgetNotConfiguredComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"kpi-widget\" [class.loading]=\"isLoading()\" [class.error]=\"error()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>...</span>\n </div>\n } @else if (error()) {\n <div class=\"error-message\">\n <span>{{ error() }}</span>\n </div>\n } @else {\n <div class=\"kpi-content\">\n <div class=\"kpi-value-container\">\n @if (config.prefix) {\n <span class=\"kpi-prefix\">{{ config.prefix }}</span>\n }\n <span class=\"kpi-value\">{{ value() }}</span>\n @if (config.suffix) {\n <span class=\"kpi-suffix\">{{ config.suffix }}</span>\n }\n </div>\n\n @if (comparisonText()) {\n <div class=\"kpi-comparison\" [ngClass]=\"trendClass()\">\n @if (config.trend) {\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"xsmall\"></kendo-svg-icon>\n }\n {{ comparisonText() }}\n </div>\n }\n\n @if (label()) {\n <div class=\"kpi-label\">{{ label() }}</div>\n }\n </div>\n\n @if (config.trend) {\n <div class=\"kpi-trend\" [ngClass]=\"trendClass()\">\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"medium\"></kendo-svg-icon>\n </div>\n }\n }\n</div>\n", styles: [".kpi-widget{display:flex;align-items:center;justify-content:center;height:100%;padding:16px;gap:16px}.kpi-content{display:flex;flex-direction:column;align-items:center;text-align:center}.kpi-value-container{display:flex;align-items:baseline;gap:4px}.kpi-prefix,.kpi-suffix{font-size:1.25rem;font-weight:500;color:var(--kendo-color-subtle, #6c757d)}.kpi-value{font-size:2.5rem;font-weight:700;color:var(--kendo-color-on-surface, #212529);line-height:1}.kpi-comparison{margin-top:4px;font-size:.75rem;font-weight:500;display:flex;align-items:center;gap:4px;justify-content:center}.kpi-comparison.trend-up{color:var(--kendo-color-success, #28a745)}.kpi-comparison.trend-down{color:var(--kendo-color-error, #dc3545)}.kpi-comparison.trend-neutral{color:var(--kendo-color-subtle, #6c757d)}.kpi-label{margin-top:8px;font-size:.875rem;color:var(--kendo-color-subtle, #6c757d)}.kpi-trend{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%}.kpi-trend.trend-up{background:#28a74526;color:var(--kendo-color-success, #28a745)}.kpi-trend.trend-down{background:#dc354526;color:var(--kendo-color-error, #dc3545)}.kpi-trend.trend-neutral{background:#6c757d26;color:var(--kendo-color-subtle, #6c757d)}@media(max-width:768px){.kpi-value{font-size:1.75rem}.kpi-prefix,.kpi-suffix{font-size:1rem}}\n"] }]
|
|
5761
|
-
}], propDecorators: { config: [{
|
|
6064
|
+
args: [{ selector: 'mm-kpi-widget', standalone: true, imports: [CommonModule, SVGIconModule, WidgetNotConfiguredComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"kpi-widget\" [class.loading]=\"isLoading()\" [class.error]=\"error() || formulaError()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>...</span>\n </div>\n } @else if (error() || formulaError()) {\n <div class=\"error-message\">\n <span>{{ error() ?? formulaError() }}</span>\n </div>\n } @else {\n <div class=\"kpi-content\">\n <div class=\"kpi-value-container\">\n @if (config.prefix) {\n <span class=\"kpi-prefix\">{{ config.prefix }}</span>\n }\n <span class=\"kpi-value\">{{ value() }}</span>\n @if (config.suffix) {\n <span class=\"kpi-suffix\">{{ config.suffix }}</span>\n }\n </div>\n\n @if (comparisonText()) {\n <div class=\"kpi-comparison\" [ngClass]=\"trendClass()\">\n @if (config.trend) {\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"xsmall\"></kendo-svg-icon>\n }\n {{ comparisonText() }}\n </div>\n }\n\n @if (label()) {\n <div class=\"kpi-label\">{{ label() }}</div>\n }\n </div>\n\n @if (config.trend) {\n <div class=\"kpi-trend\" [ngClass]=\"trendClass()\">\n <kendo-svg-icon [icon]=\"trendIcon()\" size=\"medium\"></kendo-svg-icon>\n </div>\n }\n }\n</div>\n", styles: [".kpi-widget{display:flex;align-items:center;justify-content:center;height:100%;padding:16px;gap:16px}.kpi-content{display:flex;flex-direction:column;align-items:center;text-align:center}.kpi-value-container{display:flex;align-items:baseline;gap:4px}.kpi-prefix,.kpi-suffix{font-size:1.25rem;font-weight:500;color:var(--kendo-color-subtle, #6c757d)}.kpi-value{font-size:2.5rem;font-weight:700;color:var(--kendo-color-on-surface, #212529);line-height:1}.kpi-comparison{margin-top:4px;font-size:.75rem;font-weight:500;display:flex;align-items:center;gap:4px;justify-content:center}.kpi-comparison.trend-up{color:var(--kendo-color-success, #28a745)}.kpi-comparison.trend-down{color:var(--kendo-color-error, #dc3545)}.kpi-comparison.trend-neutral{color:var(--kendo-color-subtle, #6c757d)}.kpi-label{margin-top:8px;font-size:.875rem;color:var(--kendo-color-subtle, #6c757d)}.kpi-trend{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%}.kpi-trend.trend-up{background:#28a74526;color:var(--kendo-color-success, #28a745)}.kpi-trend.trend-down{background:#dc354526;color:var(--kendo-color-error, #dc3545)}.kpi-trend.trend-neutral{background:#6c757d26;color:var(--kendo-color-subtle, #6c757d)}@media(max-width:768px){.kpi-value{font-size:1.75rem}.kpi-prefix,.kpi-suffix{font-size:1rem}}\n"] }]
|
|
6065
|
+
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
5762
6066
|
type: Input
|
|
5763
6067
|
}] } });
|
|
5764
6068
|
|
|
@@ -6002,6 +6306,8 @@ class KpiConfigDialogComponent {
|
|
|
6002
6306
|
'StreamDataQueryRow'
|
|
6003
6307
|
]);
|
|
6004
6308
|
meshBoardStateService = inject(MeshBoardStateService);
|
|
6309
|
+
variableService = inject(MeshBoardVariableService);
|
|
6310
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
6005
6311
|
windowRef = inject(WindowRef);
|
|
6006
6312
|
ckTypeSelectorInput;
|
|
6007
6313
|
entitySelectorInput;
|
|
@@ -6028,6 +6334,11 @@ class KpiConfigDialogComponent {
|
|
|
6028
6334
|
initialQueryCategoryValue;
|
|
6029
6335
|
// Initial values for static
|
|
6030
6336
|
initialStaticValue;
|
|
6337
|
+
// Initial values for formula / output variable (AB#5364)
|
|
6338
|
+
/** Id of the edited widget: excludes its own output from the checks and drives the cycle check */
|
|
6339
|
+
initialWidgetId;
|
|
6340
|
+
initialFormula;
|
|
6341
|
+
initialOutputVariableName;
|
|
6031
6342
|
// Initial values for filters
|
|
6032
6343
|
initialFilters;
|
|
6033
6344
|
// Data source type selection
|
|
@@ -6081,8 +6392,103 @@ class KpiConfigDialogComponent {
|
|
|
6081
6392
|
queryCategoryField: '',
|
|
6082
6393
|
queryCategoryValue: '',
|
|
6083
6394
|
// Static form fields
|
|
6084
|
-
staticValue: ''
|
|
6395
|
+
staticValue: '',
|
|
6396
|
+
// Formula form fields
|
|
6397
|
+
formula: '',
|
|
6398
|
+
outputVariableName: ''
|
|
6085
6399
|
};
|
|
6400
|
+
/**
|
|
6401
|
+
* Variables a formula can reference: MeshBoard variables plus other widgets' outputs.
|
|
6402
|
+
* Read live, so outputs published while the dialog is open (e.g. a query KPI that
|
|
6403
|
+
* finished loading) become available immediately.
|
|
6404
|
+
*/
|
|
6405
|
+
get formulaVariables() {
|
|
6406
|
+
return this.meshBoardStateService.getVariables()
|
|
6407
|
+
.filter(v => !(v.source === 'widget' && v.widgetId === this.initialWidgetId));
|
|
6408
|
+
}
|
|
6409
|
+
formulaValidationCache;
|
|
6410
|
+
/**
|
|
6411
|
+
* Variable names that exist by configuration even while they carry no value yet: the
|
|
6412
|
+
* outputs declared by other KPIs (e.g. still loading or in error) and the variables an
|
|
6413
|
+
* entity selector exposes once an entity is picked.
|
|
6414
|
+
*/
|
|
6415
|
+
get declaredVariableNames() {
|
|
6416
|
+
const outputs = this.meshBoardStateService.widgets()
|
|
6417
|
+
.filter(w => w.id !== this.initialWidgetId && w.type === 'kpi')
|
|
6418
|
+
.map(w => w.outputVariableName?.trim())
|
|
6419
|
+
.filter((name) => !!name);
|
|
6420
|
+
const selectorVariables = this.meshBoardStateService.getEntitySelectors().flatMap(selector => [
|
|
6421
|
+
...(selector.attributeMappings ?? []).map(m => m.variableName),
|
|
6422
|
+
`${selector.id}_rtId`,
|
|
6423
|
+
`${selector.id}_rtCkTypeId`
|
|
6424
|
+
]);
|
|
6425
|
+
return [...outputs, ...selectorVariables];
|
|
6426
|
+
}
|
|
6427
|
+
/**
|
|
6428
|
+
* Validation of the current formula (memoized per formula text and available variable names).
|
|
6429
|
+
* Declared names count as known, so a reference to an output without a value yet can be saved;
|
|
6430
|
+
* the preview keeps evaluating the live values and shows it as pending.
|
|
6431
|
+
*/
|
|
6432
|
+
get formulaValidation() {
|
|
6433
|
+
const formula = this.form.formula;
|
|
6434
|
+
const availableNames = [...new Set([...this.formulaVariables.map(v => v.name), ...this.declaredVariableNames])];
|
|
6435
|
+
const names = JSON.stringify(availableNames);
|
|
6436
|
+
if (this.formulaValidationCache?.formula !== formula || this.formulaValidationCache.names !== names) {
|
|
6437
|
+
this.formulaValidationCache = {
|
|
6438
|
+
formula,
|
|
6439
|
+
names,
|
|
6440
|
+
result: validateFormula(formula, availableNames, this.expressionEvaluator)
|
|
6441
|
+
};
|
|
6442
|
+
}
|
|
6443
|
+
return this.formulaValidationCache.result;
|
|
6444
|
+
}
|
|
6445
|
+
/** Result of the current formula over the current variable values */
|
|
6446
|
+
get formulaPreview() {
|
|
6447
|
+
const result = evaluateFormula(this.form.formula, this.formulaVariables, this.expressionEvaluator);
|
|
6448
|
+
switch (result.status) {
|
|
6449
|
+
case 'ok': return String(result.value);
|
|
6450
|
+
case 'pending': return '- (a referenced variable has no value yet)';
|
|
6451
|
+
default: return result.error;
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
/** Validation message for the output variable name, or `null` when valid or empty */
|
|
6455
|
+
get outputVariableError() {
|
|
6456
|
+
const name = this.form.outputVariableName.trim();
|
|
6457
|
+
if (!name)
|
|
6458
|
+
return null;
|
|
6459
|
+
if (!this.variableService.isValidVariableName(name)) {
|
|
6460
|
+
return 'Use letters, digits and underscores only; the name must not start with a digit.';
|
|
6461
|
+
}
|
|
6462
|
+
if ((this.meshBoardStateService.meshBoardConfig().variables ?? []).some(v => v.name === name)) {
|
|
6463
|
+
return `A MeshBoard variable named "${name}" already exists.`;
|
|
6464
|
+
}
|
|
6465
|
+
const otherProducer = this.meshBoardStateService.widgets().find(w => w.id !== this.initialWidgetId && w.type === 'kpi' && w.outputVariableName?.trim() === name);
|
|
6466
|
+
if (otherProducer) {
|
|
6467
|
+
return `Widget "${otherProducer.title}" already publishes "${name}".`;
|
|
6468
|
+
}
|
|
6469
|
+
return null;
|
|
6470
|
+
}
|
|
6471
|
+
/** Circular reference introduced by the pending formula / output name, or `null` */
|
|
6472
|
+
get cycleError() {
|
|
6473
|
+
if (this.dataSourceType !== 'formula')
|
|
6474
|
+
return null;
|
|
6475
|
+
const widgetId = this.initialWidgetId ?? '__pending-kpi__';
|
|
6476
|
+
const widgets = this.meshBoardStateService.widgets();
|
|
6477
|
+
const current = widgets.find(w => w.id === widgetId);
|
|
6478
|
+
const pending = {
|
|
6479
|
+
...(current ?? { id: widgetId, type: 'kpi', title: '', col: 1, row: 1, colSpan: 1, rowSpan: 1, valueAttribute: '', dataSource: { type: 'static' } }),
|
|
6480
|
+
valueMode: 'formula',
|
|
6481
|
+
formula: this.form.formula,
|
|
6482
|
+
outputVariableName: this.form.outputVariableName.trim() || undefined
|
|
6483
|
+
};
|
|
6484
|
+
const pendingWidgets = current
|
|
6485
|
+
? widgets.map(w => w.id === widgetId ? pending : w)
|
|
6486
|
+
: [...widgets, pending];
|
|
6487
|
+
const configuredNames = (this.meshBoardStateService.meshBoardConfig().variables ?? []).map(v => v.name);
|
|
6488
|
+
return findVariableCycles(pendingWidgets, configuredNames).has(widgetId)
|
|
6489
|
+
? 'Circular reference: the formula depends on this widget\'s own output.'
|
|
6490
|
+
: null;
|
|
6491
|
+
}
|
|
6086
6492
|
variableSyntaxHint = '${variableName}';
|
|
6087
6493
|
trendOptions = [
|
|
6088
6494
|
{ value: undefined, label: 'None' },
|
|
@@ -6096,6 +6502,11 @@ class KpiConfigDialogComponent {
|
|
|
6096
6502
|
{ value: 'groupedAggregation', label: 'Grouped Aggregation' }
|
|
6097
6503
|
];
|
|
6098
6504
|
get isValid() {
|
|
6505
|
+
if (this.outputVariableError)
|
|
6506
|
+
return false;
|
|
6507
|
+
if (this.dataSourceType === 'formula') {
|
|
6508
|
+
return this.formulaValidation.valid && !this.cycleError;
|
|
6509
|
+
}
|
|
6099
6510
|
if (this.dataSourceType === 'static') {
|
|
6100
6511
|
return (this.form.staticValue?.trim() ?? '') !== '';
|
|
6101
6512
|
}
|
|
@@ -6138,6 +6549,8 @@ class KpiConfigDialogComponent {
|
|
|
6138
6549
|
this.form.queryCategoryField = this.initialQueryCategoryField || '';
|
|
6139
6550
|
this.form.queryCategoryValue = this.initialQueryCategoryValue || '';
|
|
6140
6551
|
this.form.staticValue = this.initialStaticValue || '';
|
|
6552
|
+
this.form.formula = this.initialFormula || '';
|
|
6553
|
+
this.form.outputVariableName = this.initialOutputVariableName || '';
|
|
6141
6554
|
// Initialize filters
|
|
6142
6555
|
if (this.initialFilters && this.initialFilters.length > 0) {
|
|
6143
6556
|
this.filters = this.initialFilters.map((f, index) => ({
|
|
@@ -6300,6 +6713,20 @@ class KpiConfigDialogComponent {
|
|
|
6300
6713
|
comparisonValue: f.comparisonValue
|
|
6301
6714
|
}))
|
|
6302
6715
|
: undefined;
|
|
6716
|
+
if (this.dataSourceType === 'formula') {
|
|
6717
|
+
this.windowRef.close({
|
|
6718
|
+
dataSourceType: 'formula',
|
|
6719
|
+
ckTypeId: '',
|
|
6720
|
+
valueAttribute: '',
|
|
6721
|
+
formula: this.form.formula.trim(),
|
|
6722
|
+
prefix: this.form.prefix || undefined,
|
|
6723
|
+
suffix: this.form.suffix || undefined,
|
|
6724
|
+
trend: this.form.trend,
|
|
6725
|
+
comparisonText: this.form.comparisonText || undefined,
|
|
6726
|
+
outputVariableName: this.form.outputVariableName.trim() || undefined
|
|
6727
|
+
});
|
|
6728
|
+
return;
|
|
6729
|
+
}
|
|
6303
6730
|
if (this.dataSourceType === 'static') {
|
|
6304
6731
|
this.windowRef.close({
|
|
6305
6732
|
dataSourceType: 'static',
|
|
@@ -6309,7 +6736,8 @@ class KpiConfigDialogComponent {
|
|
|
6309
6736
|
prefix: this.form.prefix || undefined,
|
|
6310
6737
|
suffix: this.form.suffix || undefined,
|
|
6311
6738
|
trend: this.form.trend,
|
|
6312
|
-
comparisonText: this.form.comparisonText || undefined
|
|
6739
|
+
comparisonText: this.form.comparisonText || undefined,
|
|
6740
|
+
outputVariableName: this.form.outputVariableName.trim() || undefined
|
|
6313
6741
|
});
|
|
6314
6742
|
return;
|
|
6315
6743
|
}
|
|
@@ -6333,6 +6761,7 @@ class KpiConfigDialogComponent {
|
|
|
6333
6761
|
suffix: this.form.suffix || undefined,
|
|
6334
6762
|
trend: this.form.trend,
|
|
6335
6763
|
comparisonText: this.form.comparisonText || undefined,
|
|
6764
|
+
outputVariableName: this.form.outputVariableName.trim() || undefined,
|
|
6336
6765
|
filters: filtersDto
|
|
6337
6766
|
});
|
|
6338
6767
|
}
|
|
@@ -6347,6 +6776,7 @@ class KpiConfigDialogComponent {
|
|
|
6347
6776
|
suffix: this.form.suffix || undefined,
|
|
6348
6777
|
trend: this.form.trend,
|
|
6349
6778
|
comparisonText: this.form.comparisonText || undefined,
|
|
6779
|
+
outputVariableName: this.form.outputVariableName.trim() || undefined,
|
|
6350
6780
|
filters: filtersDto
|
|
6351
6781
|
});
|
|
6352
6782
|
}
|
|
@@ -6360,6 +6790,12 @@ class KpiConfigDialogComponent {
|
|
|
6360
6790
|
onDataSourceTypeChange(type) {
|
|
6361
6791
|
this.dataSourceType = type;
|
|
6362
6792
|
}
|
|
6793
|
+
/** Appends a `${name}` reference to the formula */
|
|
6794
|
+
insertVariable(name) {
|
|
6795
|
+
const formula = this.form.formula;
|
|
6796
|
+
const separator = formula && !formula.endsWith(' ') ? ' ' : '';
|
|
6797
|
+
this.form.formula = `${formula}${separator}\${${name}}`;
|
|
6798
|
+
}
|
|
6363
6799
|
// ============================================================================
|
|
6364
6800
|
// Persistent Query Methods
|
|
6365
6801
|
// ============================================================================
|
|
@@ -6489,7 +6925,7 @@ class KpiConfigDialogComponent {
|
|
|
6489
6925
|
this.filters = updatedFilters;
|
|
6490
6926
|
}
|
|
6491
6927
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6492
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
6928
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
6493
6929
|
<div class="config-container">
|
|
6494
6930
|
|
|
6495
6931
|
<div class="config-form" [class.loading]="isLoadingInitial">
|
|
@@ -6520,6 +6956,13 @@ class KpiConfigDialogComponent {
|
|
|
6520
6956
|
(click)="onDataSourceTypeChange('static')">
|
|
6521
6957
|
Static
|
|
6522
6958
|
</button>
|
|
6959
|
+
<button
|
|
6960
|
+
kendoButton
|
|
6961
|
+
[fillMode]="dataSourceType === 'formula' ? 'solid' : 'outline'"
|
|
6962
|
+
[themeColor]="dataSourceType === 'formula' ? 'primary' : 'base'"
|
|
6963
|
+
(click)="onDataSourceTypeChange('formula')">
|
|
6964
|
+
Formula
|
|
6965
|
+
</button>
|
|
6523
6966
|
</div>
|
|
6524
6967
|
</div>
|
|
6525
6968
|
|
|
@@ -6806,6 +7249,59 @@ class KpiConfigDialogComponent {
|
|
|
6806
7249
|
</div>
|
|
6807
7250
|
}
|
|
6808
7251
|
|
|
7252
|
+
<!-- ============================================================ -->
|
|
7253
|
+
<!-- FORMULA -->
|
|
7254
|
+
<!-- ============================================================ -->
|
|
7255
|
+
@if (dataSourceType === 'formula') {
|
|
7256
|
+
<div class="form-field">
|
|
7257
|
+
<label>Formula <span class="required">*</span></label>
|
|
7258
|
+
<kendo-textbox
|
|
7259
|
+
[(ngModel)]="form.formula"
|
|
7260
|
+
placeholder="e.g., (\${a} - \${b}) / 1000">
|
|
7261
|
+
</kendo-textbox>
|
|
7262
|
+
<p class="field-hint">
|
|
7263
|
+
Reference variables with <code>{{variableSyntaxHint}}</code>. Supported: <code>+ - * / % ^</code>, parentheses,
|
|
7264
|
+
<code>min</code>, <code>max</code>, <code>abs</code>, <code>round</code>, <code>clamp</code>, <code>? :</code>.
|
|
7265
|
+
Numbers use a dot as decimal separator.
|
|
7266
|
+
</p>
|
|
7267
|
+
@if (form.formula.trim() && formulaValidation.error) {
|
|
7268
|
+
<p class="field-error">{{ formulaValidation.error }}</p>
|
|
7269
|
+
} @else if (form.formula.trim()) {
|
|
7270
|
+
<p class="field-hint">Preview: <strong>{{ formulaPreview }}</strong></p>
|
|
7271
|
+
}
|
|
7272
|
+
</div>
|
|
7273
|
+
<div class="form-field">
|
|
7274
|
+
<label>Available Variables</label>
|
|
7275
|
+
@if (formulaVariables.length > 0) {
|
|
7276
|
+
<div class="variable-list">
|
|
7277
|
+
@for (variable of formulaVariables; track variable.name) {
|
|
7278
|
+
<button kendoButton fillMode="flat" size="small" type="button"
|
|
7279
|
+
[title]="'Insert ' + variable.name + ' (current value: ' + (variable.value || '-') + ')'"
|
|
7280
|
+
(click)="insertVariable(variable.name)">
|
|
7281
|
+
{{ variable.name }}@if (variable.source === 'widget') { <span class="variable-source">(widget)</span> }
|
|
7282
|
+
</button>
|
|
7283
|
+
}
|
|
7284
|
+
</div>
|
|
7285
|
+
} @else {
|
|
7286
|
+
<p class="field-hint">No variables defined. Add variables in the MeshBoard settings or publish a widget value as output variable.</p>
|
|
7287
|
+
}
|
|
7288
|
+
</div>
|
|
7289
|
+
}
|
|
7290
|
+
|
|
7291
|
+
<!-- Output Variable -->
|
|
7292
|
+
<div class="form-field">
|
|
7293
|
+
<label>Output Variable</label>
|
|
7294
|
+
<kendo-textbox [(ngModel)]="form.outputVariableName" placeholder="e.g., consumption"></kendo-textbox>
|
|
7295
|
+
@if (outputVariableError) {
|
|
7296
|
+
<p class="field-error">{{ outputVariableError }}</p>
|
|
7297
|
+
} @else {
|
|
7298
|
+
<p class="field-hint">Optional. Publishes the displayed raw value as MeshBoard variable for formulas, texts and filters of other widgets.</p>
|
|
7299
|
+
}
|
|
7300
|
+
@if (cycleError) {
|
|
7301
|
+
<p class="field-error">{{ cycleError }}</p>
|
|
7302
|
+
}
|
|
7303
|
+
</div>
|
|
7304
|
+
|
|
6809
7305
|
<!-- Display Options -->
|
|
6810
7306
|
<div class="form-section">
|
|
6811
7307
|
<h4>Display Options</h4>
|
|
@@ -6871,7 +7367,7 @@ class KpiConfigDialogComponent {
|
|
|
6871
7367
|
</button>
|
|
6872
7368
|
</div>
|
|
6873
7369
|
</div>
|
|
6874
|
-
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
7370
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
6875
7371
|
}
|
|
6876
7372
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
|
|
6877
7373
|
type: Component,
|
|
@@ -6919,6 +7415,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
6919
7415
|
(click)="onDataSourceTypeChange('static')">
|
|
6920
7416
|
Static
|
|
6921
7417
|
</button>
|
|
7418
|
+
<button
|
|
7419
|
+
kendoButton
|
|
7420
|
+
[fillMode]="dataSourceType === 'formula' ? 'solid' : 'outline'"
|
|
7421
|
+
[themeColor]="dataSourceType === 'formula' ? 'primary' : 'base'"
|
|
7422
|
+
(click)="onDataSourceTypeChange('formula')">
|
|
7423
|
+
Formula
|
|
7424
|
+
</button>
|
|
6922
7425
|
</div>
|
|
6923
7426
|
</div>
|
|
6924
7427
|
|
|
@@ -7205,6 +7708,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
7205
7708
|
</div>
|
|
7206
7709
|
}
|
|
7207
7710
|
|
|
7711
|
+
<!-- ============================================================ -->
|
|
7712
|
+
<!-- FORMULA -->
|
|
7713
|
+
<!-- ============================================================ -->
|
|
7714
|
+
@if (dataSourceType === 'formula') {
|
|
7715
|
+
<div class="form-field">
|
|
7716
|
+
<label>Formula <span class="required">*</span></label>
|
|
7717
|
+
<kendo-textbox
|
|
7718
|
+
[(ngModel)]="form.formula"
|
|
7719
|
+
placeholder="e.g., (\${a} - \${b}) / 1000">
|
|
7720
|
+
</kendo-textbox>
|
|
7721
|
+
<p class="field-hint">
|
|
7722
|
+
Reference variables with <code>{{variableSyntaxHint}}</code>. Supported: <code>+ - * / % ^</code>, parentheses,
|
|
7723
|
+
<code>min</code>, <code>max</code>, <code>abs</code>, <code>round</code>, <code>clamp</code>, <code>? :</code>.
|
|
7724
|
+
Numbers use a dot as decimal separator.
|
|
7725
|
+
</p>
|
|
7726
|
+
@if (form.formula.trim() && formulaValidation.error) {
|
|
7727
|
+
<p class="field-error">{{ formulaValidation.error }}</p>
|
|
7728
|
+
} @else if (form.formula.trim()) {
|
|
7729
|
+
<p class="field-hint">Preview: <strong>{{ formulaPreview }}</strong></p>
|
|
7730
|
+
}
|
|
7731
|
+
</div>
|
|
7732
|
+
<div class="form-field">
|
|
7733
|
+
<label>Available Variables</label>
|
|
7734
|
+
@if (formulaVariables.length > 0) {
|
|
7735
|
+
<div class="variable-list">
|
|
7736
|
+
@for (variable of formulaVariables; track variable.name) {
|
|
7737
|
+
<button kendoButton fillMode="flat" size="small" type="button"
|
|
7738
|
+
[title]="'Insert ' + variable.name + ' (current value: ' + (variable.value || '-') + ')'"
|
|
7739
|
+
(click)="insertVariable(variable.name)">
|
|
7740
|
+
{{ variable.name }}@if (variable.source === 'widget') { <span class="variable-source">(widget)</span> }
|
|
7741
|
+
</button>
|
|
7742
|
+
}
|
|
7743
|
+
</div>
|
|
7744
|
+
} @else {
|
|
7745
|
+
<p class="field-hint">No variables defined. Add variables in the MeshBoard settings or publish a widget value as output variable.</p>
|
|
7746
|
+
}
|
|
7747
|
+
</div>
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
<!-- Output Variable -->
|
|
7751
|
+
<div class="form-field">
|
|
7752
|
+
<label>Output Variable</label>
|
|
7753
|
+
<kendo-textbox [(ngModel)]="form.outputVariableName" placeholder="e.g., consumption"></kendo-textbox>
|
|
7754
|
+
@if (outputVariableError) {
|
|
7755
|
+
<p class="field-error">{{ outputVariableError }}</p>
|
|
7756
|
+
} @else {
|
|
7757
|
+
<p class="field-hint">Optional. Publishes the displayed raw value as MeshBoard variable for formulas, texts and filters of other widgets.</p>
|
|
7758
|
+
}
|
|
7759
|
+
@if (cycleError) {
|
|
7760
|
+
<p class="field-error">{{ cycleError }}</p>
|
|
7761
|
+
}
|
|
7762
|
+
</div>
|
|
7763
|
+
|
|
7208
7764
|
<!-- Display Options -->
|
|
7209
7765
|
<div class="form-section">
|
|
7210
7766
|
<h4>Display Options</h4>
|
|
@@ -7270,7 +7826,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
7270
7826
|
</button>
|
|
7271
7827
|
</div>
|
|
7272
7828
|
</div>
|
|
7273
|
-
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"] }]
|
|
7829
|
+
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"] }]
|
|
7274
7830
|
}], propDecorators: { ckTypeSelectorInput: [{
|
|
7275
7831
|
type: ViewChild,
|
|
7276
7832
|
args: ['ckTypeSelector']
|
|
@@ -7318,6 +7874,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
7318
7874
|
type: Input
|
|
7319
7875
|
}], initialStaticValue: [{
|
|
7320
7876
|
type: Input
|
|
7877
|
+
}], initialWidgetId: [{
|
|
7878
|
+
type: Input
|
|
7879
|
+
}], initialFormula: [{
|
|
7880
|
+
type: Input
|
|
7881
|
+
}], initialOutputVariableName: [{
|
|
7882
|
+
type: Input
|
|
7321
7883
|
}], initialFilters: [{
|
|
7322
7884
|
type: Input
|
|
7323
7885
|
}] } });
|
|
@@ -28425,8 +28987,10 @@ function registerDefaultWidgets(registry) {
|
|
|
28425
28987
|
const kpiWidget = widget;
|
|
28426
28988
|
const isPersistentQuery = kpiWidget.dataSource.type === 'persistentQuery';
|
|
28427
28989
|
const isStatic = kpiWidget.dataSource.type === 'static';
|
|
28428
|
-
const
|
|
28990
|
+
const isFormula = isStatic && kpiWidget.valueMode === 'formula';
|
|
28991
|
+
const dataSourceType = isPersistentQuery ? 'persistentQuery' : isFormula ? 'formula' : isStatic ? 'static' : 'runtimeEntity';
|
|
28429
28992
|
return {
|
|
28993
|
+
initialWidgetId: kpiWidget.id,
|
|
28430
28994
|
// Runtime entity fields
|
|
28431
28995
|
initialCkTypeId: getDataSourceInfo(widget).ckTypeId,
|
|
28432
28996
|
initialRtId: getDataSourceInfo(widget).rtId,
|
|
@@ -28445,6 +29009,9 @@ function registerDefaultWidgets(registry) {
|
|
|
28445
29009
|
initialQueryCategoryValue: kpiWidget.queryCategoryValue,
|
|
28446
29010
|
// Static fields
|
|
28447
29011
|
initialStaticValue: kpiWidget.staticValue,
|
|
29012
|
+
// Formula / output variable (AB#5364)
|
|
29013
|
+
initialFormula: kpiWidget.formula,
|
|
29014
|
+
initialOutputVariableName: kpiWidget.outputVariableName,
|
|
28448
29015
|
// Display options
|
|
28449
29016
|
initialPrefix: kpiWidget.prefix,
|
|
28450
29017
|
initialSuffix: kpiWidget.suffix,
|
|
@@ -28461,9 +29028,33 @@ function registerDefaultWidgets(registry) {
|
|
|
28461
29028
|
operator: String(f.operator),
|
|
28462
29029
|
comparisonValue: f.comparisonValue
|
|
28463
29030
|
}));
|
|
28464
|
-
if (result.dataSourceType === '
|
|
29031
|
+
if (result.dataSourceType === 'formula') {
|
|
29032
|
+
return {
|
|
29033
|
+
...widget,
|
|
29034
|
+
dataSource: { type: 'static' },
|
|
29035
|
+
valueMode: 'formula',
|
|
29036
|
+
formula: result.formula,
|
|
29037
|
+
outputVariableName: result.outputVariableName,
|
|
29038
|
+
valueAttribute: '',
|
|
29039
|
+
staticValue: undefined,
|
|
29040
|
+
labelAttribute: undefined,
|
|
29041
|
+
queryMode: undefined,
|
|
29042
|
+
queryValueField: undefined,
|
|
29043
|
+
queryCategoryField: undefined,
|
|
29044
|
+
queryCategoryValue: undefined,
|
|
29045
|
+
prefix: result.prefix,
|
|
29046
|
+
suffix: result.suffix,
|
|
29047
|
+
trend: result.trend,
|
|
29048
|
+
comparisonText: result.comparisonText,
|
|
29049
|
+
filters: undefined
|
|
29050
|
+
};
|
|
29051
|
+
}
|
|
29052
|
+
else if (result.dataSourceType === 'static') {
|
|
28465
29053
|
return {
|
|
28466
29054
|
...widget,
|
|
29055
|
+
valueMode: undefined,
|
|
29056
|
+
formula: undefined,
|
|
29057
|
+
outputVariableName: result.outputVariableName,
|
|
28467
29058
|
dataSource: { type: 'static' },
|
|
28468
29059
|
valueAttribute: '',
|
|
28469
29060
|
staticValue: result.staticValue,
|
|
@@ -28475,6 +29066,7 @@ function registerDefaultWidgets(registry) {
|
|
|
28475
29066
|
prefix: result.prefix,
|
|
28476
29067
|
suffix: result.suffix,
|
|
28477
29068
|
trend: result.trend,
|
|
29069
|
+
comparisonText: result.comparisonText,
|
|
28478
29070
|
filters: undefined
|
|
28479
29071
|
};
|
|
28480
29072
|
}
|
|
@@ -28490,6 +29082,9 @@ function registerDefaultWidgets(registry) {
|
|
|
28490
29082
|
};
|
|
28491
29083
|
return {
|
|
28492
29084
|
...widget,
|
|
29085
|
+
valueMode: undefined,
|
|
29086
|
+
formula: undefined,
|
|
29087
|
+
outputVariableName: result.outputVariableName,
|
|
28493
29088
|
dataSource,
|
|
28494
29089
|
valueAttribute: '',
|
|
28495
29090
|
staticValue: undefined,
|
|
@@ -28500,6 +29095,7 @@ function registerDefaultWidgets(registry) {
|
|
|
28500
29095
|
prefix: result.prefix,
|
|
28501
29096
|
suffix: result.suffix,
|
|
28502
29097
|
trend: result.trend,
|
|
29098
|
+
comparisonText: result.comparisonText,
|
|
28503
29099
|
filters
|
|
28504
29100
|
};
|
|
28505
29101
|
}
|
|
@@ -28507,6 +29103,9 @@ function registerDefaultWidgets(registry) {
|
|
|
28507
29103
|
// Create RuntimeEntityDataSource (default behavior)
|
|
28508
29104
|
return {
|
|
28509
29105
|
...widget,
|
|
29106
|
+
valueMode: undefined,
|
|
29107
|
+
formula: undefined,
|
|
29108
|
+
outputVariableName: result.outputVariableName,
|
|
28510
29109
|
dataSource: createDataSource(result, false),
|
|
28511
29110
|
valueAttribute: result.valueAttribute,
|
|
28512
29111
|
labelAttribute: result.labelAttribute,
|
|
@@ -28518,6 +29117,7 @@ function registerDefaultWidgets(registry) {
|
|
|
28518
29117
|
prefix: result.prefix,
|
|
28519
29118
|
suffix: result.suffix,
|
|
28520
29119
|
trend: result.trend,
|
|
29120
|
+
comparisonText: result.comparisonText,
|
|
28521
29121
|
filters
|
|
28522
29122
|
};
|
|
28523
29123
|
}
|
|
@@ -28549,7 +29149,11 @@ function registerDefaultWidgets(registry) {
|
|
|
28549
29149
|
suffix: widget.suffix,
|
|
28550
29150
|
icon: widget.icon,
|
|
28551
29151
|
trend: widget.trend,
|
|
29152
|
+
comparisonText: widget.comparisonText,
|
|
28552
29153
|
staticValue: widget.staticValue,
|
|
29154
|
+
valueMode: widget.valueMode,
|
|
29155
|
+
formula: widget.formula,
|
|
29156
|
+
outputVariableName: widget.outputVariableName,
|
|
28553
29157
|
queryMode: widget.queryMode,
|
|
28554
29158
|
queryValueField: widget.queryValueField,
|
|
28555
29159
|
queryCategoryField: widget.queryCategoryField,
|
|
@@ -28584,11 +29188,14 @@ function registerDefaultWidgets(registry) {
|
|
|
28584
29188
|
dataSource,
|
|
28585
29189
|
valueAttribute: '',
|
|
28586
29190
|
staticValue: config['staticValue'],
|
|
29191
|
+
valueMode: config['valueMode'] === 'formula' ? 'formula' : undefined,
|
|
29192
|
+
formula: config['formula'],
|
|
28587
29193
|
prefix: config['prefix'],
|
|
28588
29194
|
suffix: config['suffix'],
|
|
28589
29195
|
icon: config['icon'],
|
|
28590
29196
|
trend: config['trend'],
|
|
28591
|
-
comparisonText: config['comparisonText']
|
|
29197
|
+
comparisonText: config['comparisonText'],
|
|
29198
|
+
outputVariableName: config['outputVariableName']
|
|
28592
29199
|
};
|
|
28593
29200
|
}
|
|
28594
29201
|
if (dataSource.type === 'persistentQuery') {
|
|
@@ -28608,6 +29215,7 @@ function registerDefaultWidgets(registry) {
|
|
|
28608
29215
|
icon: config['icon'],
|
|
28609
29216
|
trend: config['trend'],
|
|
28610
29217
|
comparisonText: config['comparisonText'],
|
|
29218
|
+
outputVariableName: config['outputVariableName'],
|
|
28611
29219
|
filters: config['filters']
|
|
28612
29220
|
};
|
|
28613
29221
|
}
|
|
@@ -28623,6 +29231,7 @@ function registerDefaultWidgets(registry) {
|
|
|
28623
29231
|
icon: config['icon'],
|
|
28624
29232
|
trend: config['trend'],
|
|
28625
29233
|
comparisonText: config['comparisonText'],
|
|
29234
|
+
outputVariableName: config['outputVariableName'],
|
|
28626
29235
|
filters: config['filters']
|
|
28627
29236
|
};
|
|
28628
29237
|
}
|