@bygd/nc-report-ui 0.1.38 → 0.1.40

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.
@@ -54,6 +54,7 @@ import CheckIcon from '@mui/icons-material/Check';
54
54
  import CloseIcon from '@mui/icons-material/Close';
55
55
  import RestartAltIcon from '@mui/icons-material/RestartAlt';
56
56
  import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
57
+ import FunctionsIcon from '@mui/icons-material/Functions';
57
58
  import StorageIcon from '@mui/icons-material/Storage';
58
59
  import TrendingUpIcon from '@mui/icons-material/TrendingUp';
59
60
  import BoltIcon from '@mui/icons-material/Bolt';
@@ -2494,6 +2495,17 @@ const ProviderSelection = ({
2494
2495
  }, renderSelectionChain()));
2495
2496
  };
2496
2497
 
2498
+ // Replace {{key}} placeholders in a title with values from the report
2499
+ // parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
2500
+ // Unknown keys are left as-is so missing parameters remain visible.
2501
+ const interpolateTitle = (template, params) => {
2502
+ if (typeof template !== 'string' || !template.includes('{{')) return template;
2503
+ return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, key) => {
2504
+ const value = params?.[key];
2505
+ return value == null ? match : String(value);
2506
+ });
2507
+ };
2508
+
2497
2509
  // Sortable Chip Component
2498
2510
  const SortableChip$1 = ({
2499
2511
  id,
@@ -2512,6 +2524,8 @@ const SortableChip$1 = ({
2512
2524
  onUpdateTitle,
2513
2525
  onResetTitle
2514
2526
  }) => {
2527
+ const reportingContext = useReportingContextOptional();
2528
+ const parameters = reportingContext?.parameters;
2515
2529
  const [isEditing, setIsEditing] = useState(false);
2516
2530
  const [editValue, setEditValue] = useState("");
2517
2531
  const inputRef = useRef(null);
@@ -2618,7 +2632,9 @@ const SortableChip$1 = ({
2618
2632
  handleCancel();
2619
2633
  }
2620
2634
  };
2621
- const displayLabel = customTitle || label;
2635
+ const rawDisplayLabel = customTitle || label;
2636
+ const displayLabel = interpolateTitle(rawDisplayLabel, parameters);
2637
+ const interpolatedFullLabel = interpolateTitle(fullLabel, parameters);
2622
2638
  const hasCustomTitle = !!customTitle;
2623
2639
  return /*#__PURE__*/React__default.createElement("div", _extends({
2624
2640
  ref: setNodeRef,
@@ -2659,7 +2675,7 @@ const SortableChip$1 = ({
2659
2675
  minWidth: 0
2660
2676
  }
2661
2677
  }, /*#__PURE__*/React__default.createElement(Tooltip$1, {
2662
- title: fullLabel || displayLabel,
2678
+ title: interpolatedFullLabel || displayLabel,
2663
2679
  arrow: true,
2664
2680
  placement: "top"
2665
2681
  }, /*#__PURE__*/React__default.createElement(Typography$1, {
@@ -2794,6 +2810,264 @@ const SortableChip$1 = ({
2794
2810
  }
2795
2811
  }))));
2796
2812
  };
2813
+
2814
+ // Sortable chip for a Computed Dimension: a { name, value } pair independent
2815
+ // of any provider, so unlike SortableChip there's no title override or sort
2816
+ // toggle. Editing happens inline (right where the item is) rather than via
2817
+ // the top-of-page Add form, so it stays visible regardless of scroll position.
2818
+ const SortableComputedChip = ({
2819
+ id,
2820
+ index,
2821
+ name,
2822
+ value,
2823
+ onDelete,
2824
+ onMoveUp,
2825
+ onMoveDown,
2826
+ isFirst,
2827
+ isLast,
2828
+ onUpdate,
2829
+ isNameTaken
2830
+ }) => {
2831
+ const {
2832
+ attributes,
2833
+ listeners,
2834
+ setNodeRef,
2835
+ transform,
2836
+ transition,
2837
+ isDragging
2838
+ } = useSortable({
2839
+ id
2840
+ });
2841
+ const [isEditing, setIsEditing] = useState(false);
2842
+ const [editName, setEditName] = useState("");
2843
+ const [editValue, setEditValue] = useState("");
2844
+ const containerRef = useRef(null);
2845
+ const style = {
2846
+ transform: CSS.Transform.toString(transform),
2847
+ transition,
2848
+ opacity: isDragging ? 0.5 : 1,
2849
+ display: "flex",
2850
+ alignItems: "center",
2851
+ width: "100%"
2852
+ };
2853
+
2854
+ // Handle click outside to cancel
2855
+ useEffect(() => {
2856
+ const handleClickOutside = event => {
2857
+ if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
2858
+ handleCancel();
2859
+ }
2860
+ };
2861
+ if (isEditing) {
2862
+ document.addEventListener("mousedown", handleClickOutside);
2863
+ return () => {
2864
+ document.removeEventListener("mousedown", handleClickOutside);
2865
+ };
2866
+ }
2867
+ }, [isEditing]);
2868
+ const handleEditClick = () => {
2869
+ setEditName(name);
2870
+ setEditValue(value);
2871
+ setIsEditing(true);
2872
+ };
2873
+ const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), index);
2874
+ const handleSave = () => {
2875
+ const trimmedName = editName.trim();
2876
+ const trimmedValue = editValue.trim();
2877
+ if (!trimmedName || !trimmedValue) return;
2878
+ if (isNameTaken(trimmedName, index)) return;
2879
+ onUpdate(index, {
2880
+ name: trimmedName,
2881
+ value: trimmedValue
2882
+ });
2883
+ setIsEditing(false);
2884
+ };
2885
+ const handleCancel = () => {
2886
+ setIsEditing(false);
2887
+ setEditName("");
2888
+ setEditValue("");
2889
+ };
2890
+ const handleKeyDown = e => {
2891
+ if (e.key === "Enter") {
2892
+ handleSave();
2893
+ } else if (e.key === "Escape") {
2894
+ handleCancel();
2895
+ }
2896
+ };
2897
+ return /*#__PURE__*/React__default.createElement("div", _extends({
2898
+ ref: setNodeRef,
2899
+ style: style
2900
+ }, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
2901
+ ref: containerRef,
2902
+ sx: {
2903
+ display: "flex",
2904
+ alignItems: "center",
2905
+ width: "100%",
2906
+ gap: 1,
2907
+ backgroundColor: "white",
2908
+ border: "1px solid #e0e0e0",
2909
+ borderRadius: 2,
2910
+ padding: 1,
2911
+ "&:hover .hover-icons": {
2912
+ opacity: 1
2913
+ }
2914
+ }
2915
+ }, /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
2916
+ sx: {
2917
+ display: "flex",
2918
+ alignItems: "center",
2919
+ cursor: "grab",
2920
+ "&:active": {
2921
+ cursor: "grabbing"
2922
+ }
2923
+ }
2924
+ }), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
2925
+ sx: {
2926
+ cursor: "grab",
2927
+ color: "rgba(110, 110, 110, 0.62)"
2928
+ }
2929
+ })), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
2930
+ icon: /*#__PURE__*/React__default.createElement(FunctionsIcon, {
2931
+ sx: {
2932
+ fontSize: "15px !important"
2933
+ }
2934
+ }),
2935
+ label: name,
2936
+ size: "small",
2937
+ sx: {
2938
+ fontWeight: 500,
2939
+ backgroundColor: "rgba(70, 134, 128, 0.1)",
2940
+ color: "rgb(70, 134, 128)",
2941
+ flexShrink: 0
2942
+ }
2943
+ }), /*#__PURE__*/React__default.createElement(Tooltip$1, {
2944
+ title: value,
2945
+ arrow: true,
2946
+ placement: "top"
2947
+ }, /*#__PURE__*/React__default.createElement(Typography$1, {
2948
+ sx: {
2949
+ fontSize: "13px",
2950
+ color: "#666",
2951
+ fontFamily: "monospace",
2952
+ whiteSpace: "nowrap",
2953
+ overflow: "hidden",
2954
+ textOverflow: "ellipsis",
2955
+ minWidth: 0,
2956
+ flex: 1
2957
+ }
2958
+ }, value)), /*#__PURE__*/React__default.createElement(Box$1, {
2959
+ className: "hover-icons",
2960
+ sx: {
2961
+ display: "flex",
2962
+ gap: 0.5,
2963
+ opacity: 0,
2964
+ transition: "opacity 0.2s"
2965
+ }
2966
+ }, /*#__PURE__*/React__default.createElement(Tooltip$1, {
2967
+ title: "Edit",
2968
+ arrow: true,
2969
+ placement: "top"
2970
+ }, /*#__PURE__*/React__default.createElement(IconButton$1, {
2971
+ size: "small",
2972
+ onClick: handleEditClick,
2973
+ "aria-label": "edit computed dimension"
2974
+ }, /*#__PURE__*/React__default.createElement(EditIcon, {
2975
+ fontSize: "small"
2976
+ }))), /*#__PURE__*/React__default.createElement(Tooltip$1, {
2977
+ title: "Delete",
2978
+ arrow: true,
2979
+ placement: "top"
2980
+ }, /*#__PURE__*/React__default.createElement(IconButton$1, {
2981
+ size: "small",
2982
+ onClick: onDelete,
2983
+ "aria-label": "delete",
2984
+ sx: {
2985
+ color: "#ef5350"
2986
+ }
2987
+ }, /*#__PURE__*/React__default.createElement(GridDeleteIcon, {
2988
+ fontSize: "small"
2989
+ })))), /*#__PURE__*/React__default.createElement(Box$1, {
2990
+ sx: {
2991
+ flex: 1
2992
+ }
2993
+ }), /*#__PURE__*/React__default.createElement(Box$1, {
2994
+ className: "hover-icons",
2995
+ sx: {
2996
+ display: "flex",
2997
+ gap: 0.5,
2998
+ opacity: 0,
2999
+ transition: "opacity 0.2s"
3000
+ }
3001
+ }, /*#__PURE__*/React__default.createElement(IconButton$1, {
3002
+ size: "small",
3003
+ onClick: onMoveUp,
3004
+ disabled: isFirst,
3005
+ "aria-label": "move up"
3006
+ }, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
3007
+ fontSize: "small"
3008
+ })), /*#__PURE__*/React__default.createElement(IconButton$1, {
3009
+ size: "small",
3010
+ onClick: onMoveDown,
3011
+ disabled: isLast,
3012
+ "aria-label": "move down"
3013
+ }, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
3014
+ fontSize: "small"
3015
+ })))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
3016
+ value: editName,
3017
+ onChange: e => setEditName(e.target.value),
3018
+ onKeyDown: handleKeyDown,
3019
+ size: "small",
3020
+ label: "Name",
3021
+ error: nameError,
3022
+ helperText: nameError ? "Name already in use" : " ",
3023
+ sx: {
3024
+ width: "200px"
3025
+ }
3026
+ }), /*#__PURE__*/React__default.createElement(TextField, {
3027
+ value: editValue,
3028
+ onChange: e => setEditValue(e.target.value),
3029
+ onKeyDown: handleKeyDown,
3030
+ size: "small",
3031
+ label: "Value",
3032
+ helperText: " ",
3033
+ sx: {
3034
+ flex: 1,
3035
+ minWidth: "200px",
3036
+ "& .MuiInputBase-input": {
3037
+ fontFamily: "monospace"
3038
+ }
3039
+ }
3040
+ }), /*#__PURE__*/React__default.createElement(Box$1, {
3041
+ sx: {
3042
+ display: "flex",
3043
+ gap: 0.5,
3044
+ alignSelf: "flex-start",
3045
+ mt: "4px"
3046
+ }
3047
+ }, /*#__PURE__*/React__default.createElement(Tooltip$1, {
3048
+ title: "Save",
3049
+ arrow: true,
3050
+ placement: "top"
3051
+ }, /*#__PURE__*/React__default.createElement("span", null, /*#__PURE__*/React__default.createElement(IconButton$1, {
3052
+ size: "small",
3053
+ onClick: handleSave,
3054
+ color: "primary",
3055
+ "aria-label": "save computed dimension",
3056
+ disabled: !editName.trim() || !editValue.trim() || nameError
3057
+ }, /*#__PURE__*/React__default.createElement(CheckIcon, {
3058
+ fontSize: "small"
3059
+ })))), /*#__PURE__*/React__default.createElement(Tooltip$1, {
3060
+ title: "Cancel",
3061
+ arrow: true,
3062
+ placement: "top"
3063
+ }, /*#__PURE__*/React__default.createElement(IconButton$1, {
3064
+ size: "small",
3065
+ onClick: handleCancel,
3066
+ "aria-label": "cancel edit"
3067
+ }, /*#__PURE__*/React__default.createElement(CloseIcon, {
3068
+ fontSize: "small"
3069
+ })))))));
3070
+ };
2797
3071
  const Dimensions = ({
2798
3072
  providersData,
2799
3073
  rootProvider,
@@ -2805,11 +3079,21 @@ const Dimensions = ({
2805
3079
  onUpdateTitle,
2806
3080
  onResetTitle,
2807
3081
  existingMetrics = [],
2808
- existingFilters = {}
3082
+ existingFilters = {},
3083
+ savedComputedDimensions = [],
3084
+ onSaveComputedDimension,
3085
+ onUpdateComputedDimension,
3086
+ onRemoveComputedDimension,
3087
+ onReorderComputedDimensions
2809
3088
  }) => {
3089
+ const reportingContext = useReportingContextOptional();
3090
+ const parameters = reportingContext?.parameters;
2810
3091
  const [isAdding, setIsAdding] = useState(false);
2811
3092
  const [dimensionSelectionChain, setDimensionSelectionChain] = useState([]);
2812
3093
  const [selectedDimension, setSelectedDimension] = useState("");
3094
+ const [isAddingComputed, setIsAddingComputed] = useState(false);
3095
+ const [computedName, setComputedName] = useState("");
3096
+ const [computedValue, setComputedValue] = useState("");
2813
3097
 
2814
3098
  // Setup drag and drop sensors
2815
3099
  const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
@@ -2856,6 +3140,54 @@ const Dimensions = ({
2856
3140
  onReorderDimensions(newDimensions);
2857
3141
  };
2858
3142
 
3143
+ // Computed Dimensions: { name, value } pairs independent of any provider.
3144
+ // Their `value` is a raw SQL literal/expression sent to the API as-is.
3145
+ const isComputedNameTaken = (name, excludeIndex = null) => savedComputedDimensions.some((cd, i) => cd.name === name && i !== excludeIndex);
3146
+ const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
3147
+ const handleAddComputedClick = () => {
3148
+ setIsAddingComputed(true);
3149
+ setIsAdding(false);
3150
+ setComputedName("");
3151
+ setComputedValue("");
3152
+ };
3153
+ const handleCancelComputed = () => {
3154
+ setIsAddingComputed(false);
3155
+ setComputedName("");
3156
+ setComputedValue("");
3157
+ };
3158
+ const handleSaveComputed = () => {
3159
+ const trimmedName = computedName.trim();
3160
+ const trimmedValue = computedValue.trim();
3161
+ if (!trimmedName || !trimmedValue) return;
3162
+ if (isComputedNameTaken(trimmedName)) return;
3163
+ onSaveComputedDimension({
3164
+ name: trimmedName,
3165
+ value: trimmedValue
3166
+ });
3167
+ handleCancelComputed();
3168
+ };
3169
+ const handleComputedDragEnd = event => {
3170
+ const {
3171
+ active,
3172
+ over
3173
+ } = event;
3174
+ if (over && active.id !== over.id) {
3175
+ const oldIndex = savedComputedDimensions.findIndex((_, i) => i === active.id);
3176
+ const newIndex = savedComputedDimensions.findIndex((_, i) => i === over.id);
3177
+ onReorderComputedDimensions(arrayMove(savedComputedDimensions, oldIndex, newIndex));
3178
+ }
3179
+ };
3180
+ const handleMoveComputedUp = index => {
3181
+ if (index > 0) {
3182
+ onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index - 1));
3183
+ }
3184
+ };
3185
+ const handleMoveComputedDown = index => {
3186
+ if (index < savedComputedDimensions.length - 1) {
3187
+ onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index + 1));
3188
+ }
3189
+ };
3190
+
2859
3191
  // Get the current provider based on selection chain
2860
3192
  const getCurrentProvider = () => {
2861
3193
  if (!rootProvider) return null;
@@ -2919,6 +3251,7 @@ const Dimensions = ({
2919
3251
  };
2920
3252
  const handleAddClick = () => {
2921
3253
  setIsAdding(true);
3254
+ setIsAddingComputed(false);
2922
3255
  setDimensionSelectionChain([]);
2923
3256
  setSelectedDimension("");
2924
3257
  };
@@ -3047,9 +3380,10 @@ const Dimensions = ({
3047
3380
  sx: {
3048
3381
  display: "flex",
3049
3382
  justifyContent: "flex-start",
3383
+ gap: 2,
3050
3384
  mb: 2
3051
3385
  }
3052
- }, !isAdding && /*#__PURE__*/React__default.createElement(Button, {
3386
+ }, !isAdding && !isAddingComputed && /*#__PURE__*/React__default.createElement(Button, {
3053
3387
  variant: "contained",
3054
3388
  startIcon: /*#__PURE__*/React__default.createElement(GridAddIcon, null),
3055
3389
  onClick: handleAddClick,
@@ -3067,7 +3401,117 @@ const Dimensions = ({
3067
3401
  boxShadow: "none"
3068
3402
  }
3069
3403
  }
3070
- }, "Add Dimension")), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
3404
+ }, "Add Dimension"), !isAdding && !isAddingComputed && /*#__PURE__*/React__default.createElement(Button, {
3405
+ variant: "outlined",
3406
+ startIcon: /*#__PURE__*/React__default.createElement(FunctionsIcon, null),
3407
+ onClick: handleAddComputedClick,
3408
+ sx: {
3409
+ height: "40px",
3410
+ fontFamily: "system-ui",
3411
+ fontSize: "14px",
3412
+ fontWeight: 500,
3413
+ borderRadius: "8px",
3414
+ textTransform: "none",
3415
+ borderColor: "rgb(70, 134, 128)",
3416
+ color: "rgb(70, 134, 128)",
3417
+ boxShadow: "none",
3418
+ "&:hover": {
3419
+ backgroundColor: "rgba(70, 134, 128, 0.04)",
3420
+ borderColor: "rgb(50, 114, 108)",
3421
+ boxShadow: "none"
3422
+ }
3423
+ }
3424
+ }, "Add Computed Dimension")), isAddingComputed && /*#__PURE__*/React__default.createElement(Paper$1, {
3425
+ elevation: 0,
3426
+ sx: {
3427
+ p: 3,
3428
+ mb: 3,
3429
+ border: "1px solid #e0e0e0",
3430
+ borderRadius: "12px",
3431
+ backgroundColor: "white",
3432
+ boxShadow: "0px 2px 4px rgba(0,0,0,0.02)",
3433
+ fontFamily: "system-ui"
3434
+ }
3435
+ }, /*#__PURE__*/React__default.createElement(Typography$1, {
3436
+ sx: {
3437
+ fontSize: "14px",
3438
+ fontWeight: 600,
3439
+ mb: 0.5,
3440
+ color: "rgb(37, 37, 37)"
3441
+ }
3442
+ }, "Define Computed Dimension"), /*#__PURE__*/React__default.createElement(Box$1, {
3443
+ sx: {
3444
+ display: "flex",
3445
+ gap: 2,
3446
+ flexWrap: "wrap",
3447
+ mt: 2
3448
+ }
3449
+ }, /*#__PURE__*/React__default.createElement(TextField, {
3450
+ label: "Name",
3451
+ value: computedName,
3452
+ onChange: e => setComputedName(e.target.value),
3453
+ size: "small",
3454
+ sx: {
3455
+ width: "260px"
3456
+ },
3457
+ error: computedNameError,
3458
+ helperText: computedNameError ? "A computed dimension with this name already exists" : " "
3459
+ }), /*#__PURE__*/React__default.createElement(TextField, {
3460
+ label: "Value",
3461
+ value: computedValue,
3462
+ onChange: e => setComputedValue(e.target.value),
3463
+ size: "small",
3464
+ sx: {
3465
+ width: "400px",
3466
+ "& .MuiInputBase-input": {
3467
+ fontFamily: "monospace"
3468
+ }
3469
+ },
3470
+ helperText: " "
3471
+ })), /*#__PURE__*/React__default.createElement(Box$1, {
3472
+ sx: {
3473
+ display: "flex",
3474
+ justifyContent: "flex-end",
3475
+ gap: 2,
3476
+ mt: 1
3477
+ }
3478
+ }, /*#__PURE__*/React__default.createElement(Button, {
3479
+ variant: "outlined",
3480
+ onClick: handleCancelComputed,
3481
+ sx: {
3482
+ height: "40px",
3483
+ fontFamily: "system-ui",
3484
+ fontSize: "14px",
3485
+ fontWeight: 500,
3486
+ borderRadius: "8px",
3487
+ boxShadow: "none",
3488
+ borderColor: "#e0e0e0",
3489
+ color: "rgb(37, 37, 37)",
3490
+ textTransform: "none",
3491
+ "&:hover": {
3492
+ backgroundColor: "#f5f5f5",
3493
+ borderColor: "#d0d0d0"
3494
+ }
3495
+ }
3496
+ }, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
3497
+ variant: "contained",
3498
+ onClick: handleSaveComputed,
3499
+ disabled: !computedName.trim() || !computedValue.trim() || computedNameError,
3500
+ sx: {
3501
+ height: "40px",
3502
+ fontFamily: "system-ui",
3503
+ fontSize: "14px",
3504
+ fontWeight: 500,
3505
+ borderRadius: "8px",
3506
+ boxShadow: "none",
3507
+ textTransform: "none",
3508
+ backgroundColor: "rgb(70, 134, 128)",
3509
+ "&:hover": {
3510
+ backgroundColor: "rgb(50, 114, 108)",
3511
+ boxShadow: "none"
3512
+ }
3513
+ }
3514
+ }, "Save Computed Dimension"))), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
3071
3515
  elevation: 0,
3072
3516
  sx: {
3073
3517
  p: 3,
@@ -3111,7 +3555,10 @@ const Dimensions = ({
3111
3555
  mb: 3
3112
3556
  }
3113
3557
  }, /*#__PURE__*/React__default.createElement(SingleSelect, {
3114
- items: getDimensionItems(),
3558
+ items: getDimensionItems().map(item => ({
3559
+ ...item,
3560
+ value: interpolateTitle(item.value, parameters)
3561
+ })),
3115
3562
  value: selectedDimension,
3116
3563
  label: "Choose Dimension",
3117
3564
  onChange: handleDimensionChange,
@@ -3230,6 +3677,50 @@ const Dimensions = ({
3230
3677
  customTitle: titleOverrides[dim.fullPath],
3231
3678
  onUpdateTitle: onUpdateTitle,
3232
3679
  onResetTitle: onResetTitle
3680
+ })))))), savedComputedDimensions.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
3681
+ sx: {
3682
+ marginTop: 2
3683
+ }
3684
+ }, /*#__PURE__*/React__default.createElement(Typography$1, {
3685
+ variant: "h6",
3686
+ sx: {
3687
+ fontSize: "16px",
3688
+ fontWeight: 600,
3689
+ marginTop: 2,
3690
+ color: "rgb(37, 37, 37)"
3691
+ }
3692
+ }, "Saved Computed Dimensions"), /*#__PURE__*/React__default.createElement(Typography$1, {
3693
+ sx: {
3694
+ fontSize: "13px",
3695
+ color: "#666",
3696
+ marginBottom: 2
3697
+ }
3698
+ }, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(DndContext, {
3699
+ sensors: sensors,
3700
+ collisionDetection: closestCenter,
3701
+ onDragEnd: handleComputedDragEnd
3702
+ }, /*#__PURE__*/React__default.createElement(SortableContext, {
3703
+ items: savedComputedDimensions.map((_, index) => index),
3704
+ strategy: verticalListSortingStrategy
3705
+ }, /*#__PURE__*/React__default.createElement(Box$1, {
3706
+ sx: {
3707
+ display: "flex",
3708
+ flexDirection: "column",
3709
+ gap: 1
3710
+ }
3711
+ }, savedComputedDimensions.map((cd, index) => /*#__PURE__*/React__default.createElement(SortableComputedChip, {
3712
+ key: index,
3713
+ id: index,
3714
+ index: index,
3715
+ name: cd.name,
3716
+ value: cd.value,
3717
+ onUpdate: onUpdateComputedDimension,
3718
+ isNameTaken: isComputedNameTaken,
3719
+ onDelete: () => onRemoveComputedDimension(index),
3720
+ onMoveUp: () => handleMoveComputedUp(index),
3721
+ onMoveDown: () => handleMoveComputedDown(index),
3722
+ isFirst: index === 0,
3723
+ isLast: index === savedComputedDimensions.length - 1
3233
3724
  })))))));
3234
3725
  };
3235
3726
 
@@ -3295,6 +3786,8 @@ const SortableChip = ({
3295
3786
  onResetTitle,
3296
3787
  source
3297
3788
  }) => {
3789
+ const reportingContext = useReportingContextOptional();
3790
+ const parameters = reportingContext?.parameters;
3298
3791
  const [isEditing, setIsEditing] = useState(false);
3299
3792
  const [editValue, setEditValue] = useState('');
3300
3793
  const inputRef = useRef(null);
@@ -3370,7 +3863,9 @@ const SortableChip = ({
3370
3863
  handleCancel();
3371
3864
  }
3372
3865
  };
3373
- const displayLabel = customTitle || label;
3866
+ const rawDisplayLabel = customTitle || label;
3867
+ const displayLabel = interpolateTitle(rawDisplayLabel, parameters);
3868
+ const interpolatedFullLabel = interpolateTitle(fullLabel, parameters);
3374
3869
  const hasCustomTitle = !!customTitle;
3375
3870
  return /*#__PURE__*/React__default.createElement("div", _extends({
3376
3871
  ref: setNodeRef,
@@ -3419,7 +3914,7 @@ const SortableChip = ({
3419
3914
  minWidth: 0
3420
3915
  }
3421
3916
  }, /*#__PURE__*/React__default.createElement(Tooltip$1, {
3422
- title: fullLabel || displayLabel,
3917
+ title: interpolatedFullLabel || displayLabel,
3423
3918
  arrow: true,
3424
3919
  placement: "top"
3425
3920
  }, /*#__PURE__*/React__default.createElement(Typography$1, {
@@ -3558,6 +4053,8 @@ const Metrics = ({
3558
4053
  existingDimensions = [],
3559
4054
  existingFilters = {}
3560
4055
  }) => {
4056
+ const reportingContext = useReportingContextOptional();
4057
+ const parameters = reportingContext?.parameters;
3561
4058
  const [isAdding, setIsAdding] = useState(false);
3562
4059
  const [metricSelectionChain, setMetricSelectionChain] = useState([]);
3563
4060
  const [selectedMetric, setSelectedMetric] = useState('');
@@ -3848,7 +4345,10 @@ const Metrics = ({
3848
4345
  mb: 3
3849
4346
  }
3850
4347
  }, /*#__PURE__*/React__default.createElement(SingleSelect, {
3851
- items: getMetricItems(),
4348
+ items: getMetricItems().map(item => ({
4349
+ ...item,
4350
+ value: interpolateTitle(item.value, parameters)
4351
+ })),
3852
4352
  value: selectedMetric,
3853
4353
  label: "Choose Metric",
3854
4354
  onChange: handleMetricChange,
@@ -3977,6 +4477,7 @@ const Filters = ({
3977
4477
  onResetTitle
3978
4478
  }) => {
3979
4479
  const reportingContext = useReportingContextOptional();
4480
+ const parameters = reportingContext?.parameters;
3980
4481
  const [isAdding, setIsAdding] = useState(false);
3981
4482
  const [isAddingFromDimension, setIsAddingFromDimension] = useState(false); // New state for dimension filter mode
3982
4483
  const [dimensionSelectionChain, setDimensionSelectionChain] = useState([]);
@@ -4645,7 +5146,10 @@ const Filters = ({
4645
5146
  mb: 3
4646
5147
  }
4647
5148
  }, /*#__PURE__*/React__default.createElement(SingleSelect, {
4648
- items: getDimensionItems(),
5149
+ items: getDimensionItems().map(item => ({
5150
+ ...item,
5151
+ value: interpolateTitle(item.value, parameters)
5152
+ })),
4649
5153
  value: selectedDimension,
4650
5154
  label: "Choose Dimension",
4651
5155
  onChange: handleDimensionChange,
@@ -4791,7 +5295,7 @@ const Filters = ({
4791
5295
  }, /*#__PURE__*/React__default.createElement(SingleSelect, {
4792
5296
  items: existingDimensions.map(dim => ({
4793
5297
  key: dim.fullPath,
4794
- value: dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle
5298
+ value: interpolateTitle(dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle, parameters)
4795
5299
  })).sort((a, b) => a.value.localeCompare(b.value)),
4796
5300
  value: selectedExistingDimension,
4797
5301
  label: "Choose Dimension",
@@ -4954,7 +5458,9 @@ const Filters = ({
4954
5458
  }
4955
5459
 
4956
5460
  // Use filter title override if exists, otherwise use dimension title override, otherwise use default dimension title
4957
- const displayLabel = titleOverrides[fullPath] || dimensionTitleOverrides[fullPath] || filter.dimensionTitle;
5461
+ const rawDisplayLabel = titleOverrides[fullPath] || dimensionTitleOverrides[fullPath] || filter.dimensionTitle;
5462
+ const displayLabel = interpolateTitle(rawDisplayLabel, parameters);
5463
+ const tooltipPathLabel = interpolateTitle(dimensionTitleOverrides[fullPath] || filter.dimensionTitle, parameters);
4958
5464
  const hasCustomTitle = !!titleOverrides[fullPath];
4959
5465
  return /*#__PURE__*/React__default.createElement(Box$1, {
4960
5466
  key: fullPath
@@ -5028,7 +5534,7 @@ const Filters = ({
5028
5534
  }, /*#__PURE__*/React__default.createElement(GridDeleteIcon, {
5029
5535
  fontSize: "small"
5030
5536
  }))), /*#__PURE__*/React__default.createElement(Tooltip$1, {
5031
- title: /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, "Path:"), " ", formatProviderPath(filter), " \u2192 ", dimensionTitleOverrides[fullPath] || filter.dimensionTitle), /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, "Full Path:"), " ", fullPath), /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, isDate ? 'Date Range' : `Values (${valueCount})`, ":"), " ", valuesList)),
5537
+ title: /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, "Path:"), " ", formatProviderPath(filter), " \u2192 ", tooltipPathLabel), /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, "Full Path:"), " ", fullPath), /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, isDate ? 'Date Range' : `Values (${valueCount})`, ":"), " ", valuesList)),
5032
5538
  arrow: true,
5033
5539
  placement: "right"
5034
5540
  }, /*#__PURE__*/React__default.createElement(IconButton$1, {
@@ -5096,7 +5602,7 @@ const Filters = ({
5096
5602
  marginBottom: 1.5,
5097
5603
  fontWeight: 600
5098
5604
  }
5099
- }, "Edit ", isDate ? 'Date Range' : 'Values', " for: ", dimensionTitleOverrides[fullPath] || filter.dimensionTitle), isDate ? /*#__PURE__*/React__default.createElement(LocalizationProvider, {
5605
+ }, "Edit ", isDate ? 'Date Range' : 'Values', " for: ", tooltipPathLabel), isDate ? /*#__PURE__*/React__default.createElement(LocalizationProvider, {
5100
5606
  dateAdapter: AdapterDayjs
5101
5607
  }, /*#__PURE__*/React__default.createElement(Box$1, {
5102
5608
  sx: {
@@ -5199,6 +5705,7 @@ const ReportDataGrid = ({
5199
5705
  reportData,
5200
5706
  dimensions,
5201
5707
  metrics,
5708
+ computedDimensions = [],
5202
5709
  loading,
5203
5710
  onPageChange,
5204
5711
  totalRows = 0,
@@ -5207,6 +5714,8 @@ const ReportDataGrid = ({
5207
5714
  metrics: {}
5208
5715
  }
5209
5716
  }) => {
5717
+ const reportingContext = useReportingContextOptional();
5718
+ const parameters = reportingContext?.parameters;
5210
5719
  const [paginationModel, setPaginationModel] = useState({
5211
5720
  page: 0,
5212
5721
  pageSize: 50
@@ -5246,6 +5755,7 @@ const ReportDataGrid = ({
5246
5755
 
5247
5756
  // Check for title override, otherwise use the friendly dimension title from provider
5248
5757
  headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
5758
+ headerName = interpolateTitle(headerName, parameters);
5249
5759
  const dimDef = dim.dimension;
5250
5760
  cols.push({
5251
5761
  field: fieldName,
@@ -5257,6 +5767,18 @@ const ReportDataGrid = ({
5257
5767
  });
5258
5768
  });
5259
5769
 
5770
+ // Add computed dimension columns. These have no provider/relations chain,
5771
+ // so the API returns each row keyed by the computed dimension's own `name`.
5772
+ computedDimensions.forEach(cd => {
5773
+ cols.push({
5774
+ field: cd.name,
5775
+ headerName: cd.name,
5776
+ flex: 1,
5777
+ minWidth: 150,
5778
+ type: 'string'
5779
+ });
5780
+ });
5781
+
5260
5782
  // Add metric columns
5261
5783
  // The API returns metric keys in two different forms depending on the path depth:
5262
5784
  //
@@ -5296,6 +5818,7 @@ const ReportDataGrid = ({
5296
5818
 
5297
5819
  // Check for title override, otherwise use the friendly metric title from provider
5298
5820
  headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
5821
+ headerName = interpolateTitle(headerName, parameters);
5299
5822
  cols.push({
5300
5823
  field: fieldName,
5301
5824
  headerName: headerName,
@@ -5315,7 +5838,7 @@ const ReportDataGrid = ({
5315
5838
  });
5316
5839
  });
5317
5840
  return cols;
5318
- }, [dimensions, metrics, titleOverrides]);
5841
+ }, [dimensions, metrics, computedDimensions, titleOverrides, parameters]);
5319
5842
 
5320
5843
  // Transform report data to rows with unique IDs.
5321
5844
  // Keys are normalised by replacing dots with underscores so that MUI DataGrid
@@ -5421,7 +5944,8 @@ const ReportBuilder = ({
5421
5944
  const [report, setReport] = useState({
5422
5945
  dimensions: [],
5423
5946
  metrics: [],
5424
- filters: {}
5947
+ filters: {},
5948
+ computedDimensions: []
5425
5949
  });
5426
5950
  const [titleOverrides, setTitleOverrides] = useState({
5427
5951
  dimensions: {},
@@ -5462,7 +5986,7 @@ const ReportBuilder = ({
5462
5986
 
5463
5987
  // Auto-run report when autoRun is true and report is loaded
5464
5988
  useEffect(() => {
5465
- if (autoRun && reportLoaded && selectedProvider && report.dimensions.length > 0) {
5989
+ if (autoRun && reportLoaded && selectedProvider && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0)) {
5466
5990
  handleRunReport();
5467
5991
  }
5468
5992
  }, [autoRun, reportLoaded]);
@@ -5688,11 +6212,16 @@ const ReportBuilder = ({
5688
6212
  });
5689
6213
  }
5690
6214
 
6215
+ // Computed dimensions are self-contained { name, value } pairs with no
6216
+ // provider/relation chain, so they're loaded as-is (no reconstruction needed)
6217
+ const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
6218
+
5691
6219
  // Set the report state
5692
6220
  setReport({
5693
6221
  dimensions: reconstructedDimensions,
5694
6222
  metrics: reconstructedMetrics,
5695
- filters: loadedFilters
6223
+ filters: loadedFilters,
6224
+ computedDimensions
5696
6225
  });
5697
6226
 
5698
6227
  // Set title overrides
@@ -5798,11 +6327,16 @@ const ReportBuilder = ({
5798
6327
  });
5799
6328
  }
5800
6329
 
6330
+ // Computed dimensions are self-contained { name, value } pairs with no
6331
+ // provider/relation chain, so they're loaded as-is (no reconstruction needed)
6332
+ const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
6333
+
5801
6334
  // Set the report state
5802
6335
  setReport({
5803
6336
  dimensions: reconstructedDimensions,
5804
6337
  metrics: reconstructedMetrics,
5805
- filters: loadedFilters
6338
+ filters: loadedFilters,
6339
+ computedDimensions
5806
6340
  });
5807
6341
 
5808
6342
  // Set title overrides
@@ -5834,7 +6368,8 @@ const ReportBuilder = ({
5834
6368
  setReport({
5835
6369
  dimensions: [],
5836
6370
  metrics: [],
5837
- filters: {}
6371
+ filters: {},
6372
+ computedDimensions: []
5838
6373
  });
5839
6374
  // Reset title overrides
5840
6375
  setTitleOverrides({
@@ -5930,6 +6465,34 @@ const ReportBuilder = ({
5930
6465
  dimensions: newOrder
5931
6466
  }));
5932
6467
  };
6468
+ const handleSaveComputedDimension = computedDimensionData => {
6469
+ setReport(prev => ({
6470
+ ...prev,
6471
+ computedDimensions: [...prev.computedDimensions, computedDimensionData]
6472
+ }));
6473
+ };
6474
+ const handleUpdateComputedDimension = (index, computedDimensionData) => {
6475
+ setReport(prev => {
6476
+ const newComputedDimensions = [...prev.computedDimensions];
6477
+ newComputedDimensions[index] = computedDimensionData;
6478
+ return {
6479
+ ...prev,
6480
+ computedDimensions: newComputedDimensions
6481
+ };
6482
+ });
6483
+ };
6484
+ const handleRemoveComputedDimension = index => {
6485
+ setReport(prev => ({
6486
+ ...prev,
6487
+ computedDimensions: prev.computedDimensions.filter((_, i) => i !== index)
6488
+ }));
6489
+ };
6490
+ const handleReorderComputedDimensions = newOrder => {
6491
+ setReport(prev => ({
6492
+ ...prev,
6493
+ computedDimensions: newOrder
6494
+ }));
6495
+ };
5933
6496
  const handleSaveMetric = metricData => {
5934
6497
  setReport(prev => {
5935
6498
  const newReport = {
@@ -6049,6 +6612,14 @@ const ReportBuilder = ({
6049
6612
  offset: page * size
6050
6613
  };
6051
6614
 
6615
+ // Only add computed_dimensions if there are any
6616
+ if (report.computedDimensions && report.computedDimensions.length > 0) {
6617
+ queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
6618
+ name: cd.name,
6619
+ value: cd.value
6620
+ }));
6621
+ }
6622
+
6052
6623
  // Only add titles if there are any overrides
6053
6624
  if (Object.keys(titles).length > 0) {
6054
6625
  queryObj.titles = titles;
@@ -6145,8 +6716,8 @@ const ReportBuilder = ({
6145
6716
  notify.warning("Please enter a report title");
6146
6717
  return;
6147
6718
  }
6148
- if (report.dimensions.length === 0 && report.metrics.length === 0) {
6149
- notify.warning("Please add at least one dimension or metric");
6719
+ if (report.dimensions.length === 0 && report.metrics.length === 0 && report.computedDimensions.length === 0) {
6720
+ notify.warning("Please add at least one dimension, metric, or computed dimension");
6150
6721
  return;
6151
6722
  }
6152
6723
  try {
@@ -6181,6 +6752,14 @@ const ReportBuilder = ({
6181
6752
  order_by: orderBy
6182
6753
  };
6183
6754
 
6755
+ // Only add computed_dimensions if there are any
6756
+ if (report.computedDimensions && report.computedDimensions.length > 0) {
6757
+ queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
6758
+ name: cd.name,
6759
+ value: cd.value
6760
+ }));
6761
+ }
6762
+
6184
6763
  // Only add titles if there are any overrides
6185
6764
  if (Object.keys(titles).length > 0) {
6186
6765
  queryObj.titles = titles;
@@ -6257,7 +6836,7 @@ const ReportBuilder = ({
6257
6836
  setSaving(false);
6258
6837
  }
6259
6838
  };
6260
- const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0);
6839
+ const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0);
6261
6840
  return /*#__PURE__*/React__default.createElement(Box$1, {
6262
6841
  sx: {
6263
6842
  p: 3,
@@ -6430,7 +7009,7 @@ const ReportBuilder = ({
6430
7009
  }
6431
7010
  }, /*#__PURE__*/React__default.createElement(Tab, {
6432
7011
  label: /*#__PURE__*/React__default.createElement(Badge, {
6433
- badgeContent: report.dimensions.length,
7012
+ badgeContent: report.dimensions.length + report.computedDimensions.length,
6434
7013
  sx: {
6435
7014
  "& .MuiBadge-badge": {
6436
7015
  backgroundColor: "rgb(70, 134, 128)",
@@ -6440,7 +7019,7 @@ const ReportBuilder = ({
6440
7019
  }
6441
7020
  }, /*#__PURE__*/React__default.createElement("span", {
6442
7021
  style: {
6443
- marginRight: report.dimensions.length > 0 ? "12px" : "0"
7022
+ marginRight: report.dimensions.length + report.computedDimensions.length > 0 ? "12px" : "0"
6444
7023
  }
6445
7024
  }, "Dimensions")),
6446
7025
  id: "report-tab-0",
@@ -6541,7 +7120,12 @@ const ReportBuilder = ({
6541
7120
  onUpdateTitle: handleUpdateDimensionTitle,
6542
7121
  onResetTitle: handleResetDimensionTitle,
6543
7122
  existingMetrics: report.metrics,
6544
- existingFilters: report.filters
7123
+ existingFilters: report.filters,
7124
+ savedComputedDimensions: report.computedDimensions,
7125
+ onSaveComputedDimension: handleSaveComputedDimension,
7126
+ onUpdateComputedDimension: handleUpdateComputedDimension,
7127
+ onRemoveComputedDimension: handleRemoveComputedDimension,
7128
+ onReorderComputedDimensions: handleReorderComputedDimensions
6545
7129
  })), /*#__PURE__*/React__default.createElement(TabPanel, {
6546
7130
  value: activeTab,
6547
7131
  index: 1
@@ -6579,6 +7163,7 @@ const ReportBuilder = ({
6579
7163
  reportData: reportData,
6580
7164
  dimensions: report.dimensions,
6581
7165
  metrics: report.metrics,
7166
+ computedDimensions: report.computedDimensions,
6582
7167
  loading: loading,
6583
7168
  onPageChange: handlePageChange,
6584
7169
  totalRows: totalRows,