@bygd/nc-report-ui 0.1.39 → 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';
@@ -2809,6 +2810,264 @@ const SortableChip$1 = ({
2809
2810
  }
2810
2811
  }))));
2811
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
+ };
2812
3071
  const Dimensions = ({
2813
3072
  providersData,
2814
3073
  rootProvider,
@@ -2820,13 +3079,21 @@ const Dimensions = ({
2820
3079
  onUpdateTitle,
2821
3080
  onResetTitle,
2822
3081
  existingMetrics = [],
2823
- existingFilters = {}
3082
+ existingFilters = {},
3083
+ savedComputedDimensions = [],
3084
+ onSaveComputedDimension,
3085
+ onUpdateComputedDimension,
3086
+ onRemoveComputedDimension,
3087
+ onReorderComputedDimensions
2824
3088
  }) => {
2825
3089
  const reportingContext = useReportingContextOptional();
2826
3090
  const parameters = reportingContext?.parameters;
2827
3091
  const [isAdding, setIsAdding] = useState(false);
2828
3092
  const [dimensionSelectionChain, setDimensionSelectionChain] = useState([]);
2829
3093
  const [selectedDimension, setSelectedDimension] = useState("");
3094
+ const [isAddingComputed, setIsAddingComputed] = useState(false);
3095
+ const [computedName, setComputedName] = useState("");
3096
+ const [computedValue, setComputedValue] = useState("");
2830
3097
 
2831
3098
  // Setup drag and drop sensors
2832
3099
  const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
@@ -2873,6 +3140,54 @@ const Dimensions = ({
2873
3140
  onReorderDimensions(newDimensions);
2874
3141
  };
2875
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
+
2876
3191
  // Get the current provider based on selection chain
2877
3192
  const getCurrentProvider = () => {
2878
3193
  if (!rootProvider) return null;
@@ -2936,6 +3251,7 @@ const Dimensions = ({
2936
3251
  };
2937
3252
  const handleAddClick = () => {
2938
3253
  setIsAdding(true);
3254
+ setIsAddingComputed(false);
2939
3255
  setDimensionSelectionChain([]);
2940
3256
  setSelectedDimension("");
2941
3257
  };
@@ -3064,9 +3380,10 @@ const Dimensions = ({
3064
3380
  sx: {
3065
3381
  display: "flex",
3066
3382
  justifyContent: "flex-start",
3383
+ gap: 2,
3067
3384
  mb: 2
3068
3385
  }
3069
- }, !isAdding && /*#__PURE__*/React__default.createElement(Button, {
3386
+ }, !isAdding && !isAddingComputed && /*#__PURE__*/React__default.createElement(Button, {
3070
3387
  variant: "contained",
3071
3388
  startIcon: /*#__PURE__*/React__default.createElement(GridAddIcon, null),
3072
3389
  onClick: handleAddClick,
@@ -3084,7 +3401,117 @@ const Dimensions = ({
3084
3401
  boxShadow: "none"
3085
3402
  }
3086
3403
  }
3087
- }, "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, {
3088
3515
  elevation: 0,
3089
3516
  sx: {
3090
3517
  p: 3,
@@ -3250,6 +3677,50 @@ const Dimensions = ({
3250
3677
  customTitle: titleOverrides[dim.fullPath],
3251
3678
  onUpdateTitle: onUpdateTitle,
3252
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
3253
3724
  })))))));
3254
3725
  };
3255
3726
 
@@ -5234,6 +5705,7 @@ const ReportDataGrid = ({
5234
5705
  reportData,
5235
5706
  dimensions,
5236
5707
  metrics,
5708
+ computedDimensions = [],
5237
5709
  loading,
5238
5710
  onPageChange,
5239
5711
  totalRows = 0,
@@ -5295,6 +5767,18 @@ const ReportDataGrid = ({
5295
5767
  });
5296
5768
  });
5297
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
+
5298
5782
  // Add metric columns
5299
5783
  // The API returns metric keys in two different forms depending on the path depth:
5300
5784
  //
@@ -5354,7 +5838,7 @@ const ReportDataGrid = ({
5354
5838
  });
5355
5839
  });
5356
5840
  return cols;
5357
- }, [dimensions, metrics, titleOverrides, parameters]);
5841
+ }, [dimensions, metrics, computedDimensions, titleOverrides, parameters]);
5358
5842
 
5359
5843
  // Transform report data to rows with unique IDs.
5360
5844
  // Keys are normalised by replacing dots with underscores so that MUI DataGrid
@@ -5460,7 +5944,8 @@ const ReportBuilder = ({
5460
5944
  const [report, setReport] = useState({
5461
5945
  dimensions: [],
5462
5946
  metrics: [],
5463
- filters: {}
5947
+ filters: {},
5948
+ computedDimensions: []
5464
5949
  });
5465
5950
  const [titleOverrides, setTitleOverrides] = useState({
5466
5951
  dimensions: {},
@@ -5501,7 +5986,7 @@ const ReportBuilder = ({
5501
5986
 
5502
5987
  // Auto-run report when autoRun is true and report is loaded
5503
5988
  useEffect(() => {
5504
- 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)) {
5505
5990
  handleRunReport();
5506
5991
  }
5507
5992
  }, [autoRun, reportLoaded]);
@@ -5727,11 +6212,16 @@ const ReportBuilder = ({
5727
6212
  });
5728
6213
  }
5729
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
+
5730
6219
  // Set the report state
5731
6220
  setReport({
5732
6221
  dimensions: reconstructedDimensions,
5733
6222
  metrics: reconstructedMetrics,
5734
- filters: loadedFilters
6223
+ filters: loadedFilters,
6224
+ computedDimensions
5735
6225
  });
5736
6226
 
5737
6227
  // Set title overrides
@@ -5837,11 +6327,16 @@ const ReportBuilder = ({
5837
6327
  });
5838
6328
  }
5839
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
+
5840
6334
  // Set the report state
5841
6335
  setReport({
5842
6336
  dimensions: reconstructedDimensions,
5843
6337
  metrics: reconstructedMetrics,
5844
- filters: loadedFilters
6338
+ filters: loadedFilters,
6339
+ computedDimensions
5845
6340
  });
5846
6341
 
5847
6342
  // Set title overrides
@@ -5873,7 +6368,8 @@ const ReportBuilder = ({
5873
6368
  setReport({
5874
6369
  dimensions: [],
5875
6370
  metrics: [],
5876
- filters: {}
6371
+ filters: {},
6372
+ computedDimensions: []
5877
6373
  });
5878
6374
  // Reset title overrides
5879
6375
  setTitleOverrides({
@@ -5969,6 +6465,34 @@ const ReportBuilder = ({
5969
6465
  dimensions: newOrder
5970
6466
  }));
5971
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
+ };
5972
6496
  const handleSaveMetric = metricData => {
5973
6497
  setReport(prev => {
5974
6498
  const newReport = {
@@ -6088,6 +6612,14 @@ const ReportBuilder = ({
6088
6612
  offset: page * size
6089
6613
  };
6090
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
+
6091
6623
  // Only add titles if there are any overrides
6092
6624
  if (Object.keys(titles).length > 0) {
6093
6625
  queryObj.titles = titles;
@@ -6184,8 +6716,8 @@ const ReportBuilder = ({
6184
6716
  notify.warning("Please enter a report title");
6185
6717
  return;
6186
6718
  }
6187
- if (report.dimensions.length === 0 && report.metrics.length === 0) {
6188
- 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");
6189
6721
  return;
6190
6722
  }
6191
6723
  try {
@@ -6220,6 +6752,14 @@ const ReportBuilder = ({
6220
6752
  order_by: orderBy
6221
6753
  };
6222
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
+
6223
6763
  // Only add titles if there are any overrides
6224
6764
  if (Object.keys(titles).length > 0) {
6225
6765
  queryObj.titles = titles;
@@ -6296,7 +6836,7 @@ const ReportBuilder = ({
6296
6836
  setSaving(false);
6297
6837
  }
6298
6838
  };
6299
- 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);
6300
6840
  return /*#__PURE__*/React__default.createElement(Box$1, {
6301
6841
  sx: {
6302
6842
  p: 3,
@@ -6469,7 +7009,7 @@ const ReportBuilder = ({
6469
7009
  }
6470
7010
  }, /*#__PURE__*/React__default.createElement(Tab, {
6471
7011
  label: /*#__PURE__*/React__default.createElement(Badge, {
6472
- badgeContent: report.dimensions.length,
7012
+ badgeContent: report.dimensions.length + report.computedDimensions.length,
6473
7013
  sx: {
6474
7014
  "& .MuiBadge-badge": {
6475
7015
  backgroundColor: "rgb(70, 134, 128)",
@@ -6479,7 +7019,7 @@ const ReportBuilder = ({
6479
7019
  }
6480
7020
  }, /*#__PURE__*/React__default.createElement("span", {
6481
7021
  style: {
6482
- marginRight: report.dimensions.length > 0 ? "12px" : "0"
7022
+ marginRight: report.dimensions.length + report.computedDimensions.length > 0 ? "12px" : "0"
6483
7023
  }
6484
7024
  }, "Dimensions")),
6485
7025
  id: "report-tab-0",
@@ -6580,7 +7120,12 @@ const ReportBuilder = ({
6580
7120
  onUpdateTitle: handleUpdateDimensionTitle,
6581
7121
  onResetTitle: handleResetDimensionTitle,
6582
7122
  existingMetrics: report.metrics,
6583
- existingFilters: report.filters
7123
+ existingFilters: report.filters,
7124
+ savedComputedDimensions: report.computedDimensions,
7125
+ onSaveComputedDimension: handleSaveComputedDimension,
7126
+ onUpdateComputedDimension: handleUpdateComputedDimension,
7127
+ onRemoveComputedDimension: handleRemoveComputedDimension,
7128
+ onReorderComputedDimensions: handleReorderComputedDimensions
6584
7129
  })), /*#__PURE__*/React__default.createElement(TabPanel, {
6585
7130
  value: activeTab,
6586
7131
  index: 1
@@ -6618,6 +7163,7 @@ const ReportBuilder = ({
6618
7163
  reportData: reportData,
6619
7164
  dimensions: report.dimensions,
6620
7165
  metrics: report.metrics,
7166
+ computedDimensions: report.computedDimensions,
6621
7167
  loading: loading,
6622
7168
  onPageChange: handlePageChange,
6623
7169
  totalRows: totalRows,