@bygd/nc-report-ui 0.1.41 → 0.1.43
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.
- package/dist/app/esm/index.js +970 -529
- package/dist/default/cjs/index.cjs +963 -528
- package/dist/default/esm/index.js +962 -529
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ var reactIntersectionObserver = require('react-intersection-observer');
|
|
|
20
20
|
var material = require('@mui/material');
|
|
21
21
|
var CheckBoxOutlineBlankIcon = require('@mui/icons-material/CheckBoxOutlineBlank');
|
|
22
22
|
var CheckBoxIcon = require('@mui/icons-material/CheckBox');
|
|
23
|
+
var CheckCircleIcon = require('@mui/icons-material/CheckCircle');
|
|
23
24
|
var EventEmitter = require('eventemitter3');
|
|
24
25
|
var Grid = require('@mui/material/Grid');
|
|
25
26
|
var IconButton = require('@mui/material/IconButton');
|
|
@@ -67,6 +68,7 @@ var LocalizationProvider = require('@mui/x-date-pickers/LocalizationProvider');
|
|
|
67
68
|
var DatePicker = require('@mui/x-date-pickers/DatePicker');
|
|
68
69
|
var AdapterDayjs = require('@mui/x-date-pickers/AdapterDayjs');
|
|
69
70
|
var dayjs = require('dayjs');
|
|
71
|
+
var CalculateIcon = require('@mui/icons-material/Calculate');
|
|
70
72
|
var styles = require('@mui/material/styles');
|
|
71
73
|
|
|
72
74
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -104,6 +106,7 @@ var Select__default = /*#__PURE__*/_interopDefault(Select);
|
|
|
104
106
|
var MenuItem__default = /*#__PURE__*/_interopDefault(MenuItem);
|
|
105
107
|
var CheckBoxOutlineBlankIcon__default = /*#__PURE__*/_interopDefault(CheckBoxOutlineBlankIcon);
|
|
106
108
|
var CheckBoxIcon__default = /*#__PURE__*/_interopDefault(CheckBoxIcon);
|
|
109
|
+
var CheckCircleIcon__default = /*#__PURE__*/_interopDefault(CheckCircleIcon);
|
|
107
110
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
108
111
|
var Grid__default = /*#__PURE__*/_interopDefault(Grid);
|
|
109
112
|
var IconButton__default = /*#__PURE__*/_interopDefault(IconButton);
|
|
@@ -144,6 +147,7 @@ var BoltIcon__default = /*#__PURE__*/_interopDefault(BoltIcon);
|
|
|
144
147
|
var FilterAltIcon__default = /*#__PURE__*/_interopDefault(FilterAltIcon);
|
|
145
148
|
var InfoOutlinedIcon__default = /*#__PURE__*/_interopDefault(InfoOutlinedIcon);
|
|
146
149
|
var dayjs__default = /*#__PURE__*/_interopDefault(dayjs);
|
|
150
|
+
var CalculateIcon__default = /*#__PURE__*/_interopDefault(CalculateIcon);
|
|
147
151
|
|
|
148
152
|
function _extends() {
|
|
149
153
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -1133,7 +1137,8 @@ function SingleSelect({
|
|
|
1133
1137
|
key,
|
|
1134
1138
|
value,
|
|
1135
1139
|
disabled,
|
|
1136
|
-
icon
|
|
1140
|
+
icon,
|
|
1141
|
+
alreadySelected
|
|
1137
1142
|
} = itm;
|
|
1138
1143
|
return /*#__PURE__*/React__namespace.default.createElement(MenuItem__default.default, {
|
|
1139
1144
|
key: key,
|
|
@@ -1144,7 +1149,12 @@ function SingleSelect({
|
|
|
1144
1149
|
minHeight: "36px",
|
|
1145
1150
|
display: "flex",
|
|
1146
1151
|
alignItems: "center",
|
|
1147
|
-
gap: "6px"
|
|
1152
|
+
gap: "6px",
|
|
1153
|
+
...(alreadySelected && {
|
|
1154
|
+
color: "rgb(70, 134, 128)",
|
|
1155
|
+
backgroundColor: "rgba(70, 134, 128, 0.08)",
|
|
1156
|
+
fontWeight: 600
|
|
1157
|
+
})
|
|
1148
1158
|
}
|
|
1149
1159
|
}, icon && /*#__PURE__*/React__namespace.default.createElement(Box__default.default, {
|
|
1150
1160
|
component: "span",
|
|
@@ -1152,7 +1162,13 @@ function SingleSelect({
|
|
|
1152
1162
|
display: "inline-flex",
|
|
1153
1163
|
alignItems: "center"
|
|
1154
1164
|
}
|
|
1155
|
-
}, icon),
|
|
1165
|
+
}, icon), alreadySelected && /*#__PURE__*/React__namespace.default.createElement(CheckCircleIcon__default.default, {
|
|
1166
|
+
fontSize: "small",
|
|
1167
|
+
sx: {
|
|
1168
|
+
color: "rgb(70, 134, 128)",
|
|
1169
|
+
fontSize: "16px"
|
|
1170
|
+
}
|
|
1171
|
+
}), formatLabel(value));
|
|
1156
1172
|
}))));
|
|
1157
1173
|
}
|
|
1158
1174
|
|
|
@@ -2607,6 +2623,163 @@ const ProviderSelection = ({
|
|
|
2607
2623
|
}, renderSelectionChain()));
|
|
2608
2624
|
};
|
|
2609
2625
|
|
|
2626
|
+
// Shared drag-and-drop primitives for the index-identified, arrow-reorderable
|
|
2627
|
+
// lists used across the report builder (Dimensions, Metrics, Column Order).
|
|
2628
|
+
// Previously duplicated per-tab; consolidated here so all three behave and
|
|
2629
|
+
// look identical and only need to be fixed in one place.
|
|
2630
|
+
|
|
2631
|
+
const useSortableListSensors = () => core.useSensors(core.useSensor(core.PointerSensor), core.useSensor(core.KeyboardSensor, {
|
|
2632
|
+
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
2633
|
+
}));
|
|
2634
|
+
|
|
2635
|
+
// Builds drag/move handlers for a list where dnd-kit item ids are the array
|
|
2636
|
+
// index. `onReorder` receives the whole reordered list.
|
|
2637
|
+
const makeListReorderHandlers = (list, onReorder) => {
|
|
2638
|
+
const handleDragEnd = ({
|
|
2639
|
+
active,
|
|
2640
|
+
over
|
|
2641
|
+
}) => {
|
|
2642
|
+
if (over && active.id !== over.id) {
|
|
2643
|
+
const oldIndex = list.findIndex((_, i) => i === active.id);
|
|
2644
|
+
const newIndex = list.findIndex((_, i) => i === over.id);
|
|
2645
|
+
onReorder(sortable.arrayMove(list, oldIndex, newIndex));
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
const handleMoveUp = index => {
|
|
2649
|
+
if (index > 0) {
|
|
2650
|
+
onReorder(sortable.arrayMove(list, index, index - 1));
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
const handleMoveDown = index => {
|
|
2654
|
+
if (index < list.length - 1) {
|
|
2655
|
+
onReorder(sortable.arrayMove(list, index, index + 1));
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
return {
|
|
2659
|
+
handleDragEnd,
|
|
2660
|
+
handleMoveUp,
|
|
2661
|
+
handleMoveDown
|
|
2662
|
+
};
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
// Wraps DndContext + SortableContext + the vertical list container.
|
|
2666
|
+
// `itemIds` must line up with the ids passed to each item's useSortable().
|
|
2667
|
+
const SortableListContext = ({
|
|
2668
|
+
sensors,
|
|
2669
|
+
itemIds,
|
|
2670
|
+
onDragEnd,
|
|
2671
|
+
children
|
|
2672
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(core.DndContext, {
|
|
2673
|
+
sensors: sensors,
|
|
2674
|
+
collisionDetection: core.closestCenter,
|
|
2675
|
+
onDragEnd: onDragEnd
|
|
2676
|
+
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
2677
|
+
items: itemIds,
|
|
2678
|
+
strategy: sortable.verticalListSortingStrategy
|
|
2679
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2680
|
+
sx: {
|
|
2681
|
+
display: "flex",
|
|
2682
|
+
flexDirection: "column",
|
|
2683
|
+
gap: 1
|
|
2684
|
+
}
|
|
2685
|
+
}, children)));
|
|
2686
|
+
|
|
2687
|
+
// Per-row useSortable() wiring shared by every sortable chip/row component.
|
|
2688
|
+
const useSortableRow = id => {
|
|
2689
|
+
const {
|
|
2690
|
+
attributes,
|
|
2691
|
+
listeners,
|
|
2692
|
+
setNodeRef,
|
|
2693
|
+
transform,
|
|
2694
|
+
transition,
|
|
2695
|
+
isDragging
|
|
2696
|
+
} = sortable.useSortable({
|
|
2697
|
+
id
|
|
2698
|
+
});
|
|
2699
|
+
const style = {
|
|
2700
|
+
transform: utilities.CSS.Transform.toString(transform),
|
|
2701
|
+
transition,
|
|
2702
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2703
|
+
display: "flex",
|
|
2704
|
+
alignItems: "center",
|
|
2705
|
+
width: "100%"
|
|
2706
|
+
};
|
|
2707
|
+
return {
|
|
2708
|
+
attributes,
|
|
2709
|
+
listeners,
|
|
2710
|
+
setNodeRef,
|
|
2711
|
+
style
|
|
2712
|
+
};
|
|
2713
|
+
};
|
|
2714
|
+
const DragHandle = ({
|
|
2715
|
+
listeners
|
|
2716
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(material.Box, _extends({}, listeners, {
|
|
2717
|
+
sx: {
|
|
2718
|
+
display: "flex",
|
|
2719
|
+
alignItems: "center",
|
|
2720
|
+
cursor: "grab",
|
|
2721
|
+
"&:active": {
|
|
2722
|
+
cursor: "grabbing"
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
2726
|
+
sx: {
|
|
2727
|
+
cursor: "grab",
|
|
2728
|
+
color: "rgba(110, 110, 110, 0.62)"
|
|
2729
|
+
}
|
|
2730
|
+
}));
|
|
2731
|
+
const MoveButtons = ({
|
|
2732
|
+
onMoveUp,
|
|
2733
|
+
onMoveDown,
|
|
2734
|
+
isFirst,
|
|
2735
|
+
isLast
|
|
2736
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2737
|
+
className: "hover-icons",
|
|
2738
|
+
sx: {
|
|
2739
|
+
display: "flex",
|
|
2740
|
+
gap: 0.5,
|
|
2741
|
+
opacity: 0,
|
|
2742
|
+
transition: "opacity 0.2s"
|
|
2743
|
+
}
|
|
2744
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2745
|
+
size: "small",
|
|
2746
|
+
onClick: onMoveUp,
|
|
2747
|
+
disabled: isFirst,
|
|
2748
|
+
"aria-label": "move up"
|
|
2749
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
2750
|
+
fontSize: "small"
|
|
2751
|
+
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2752
|
+
size: "small",
|
|
2753
|
+
onClick: onMoveDown,
|
|
2754
|
+
disabled: isLast,
|
|
2755
|
+
"aria-label": "move down"
|
|
2756
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
2757
|
+
fontSize: "small"
|
|
2758
|
+
})));
|
|
2759
|
+
|
|
2760
|
+
// Builds an identity string for a saved dimension/metric instance. Deliberate
|
|
2761
|
+
// duplicates (the same field added more than once) are allowed - the first
|
|
2762
|
+
// occurrence keeps the plain fullPath (so existing saved reports and their
|
|
2763
|
+
// title-override keys stay backward compatible), later occurrences get a
|
|
2764
|
+
// `#n` suffix.
|
|
2765
|
+
const makeInstanceId = (fullPath, occurrenceIndex) => occurrenceIndex === 0 ? fullPath : `${fullPath}#${occurrenceIndex}`;
|
|
2766
|
+
|
|
2767
|
+
// Picks the next free instance id for a newly-added fullPath, given the
|
|
2768
|
+
// items already saved. Scans occurrence numbers from 0 rather than just
|
|
2769
|
+
// counting existing instances with this fullPath, because counting alone
|
|
2770
|
+
// can collide: e.g. add "ft.currency" twice (ids "ft.currency",
|
|
2771
|
+
// "ft.currency#1"), remove the first, then add "ft.currency" again - a
|
|
2772
|
+
// plain count of 1 would regenerate "ft.currency#1", clashing with the
|
|
2773
|
+
// surviving duplicate.
|
|
2774
|
+
const nextInstanceId = (fullPath, existingItems) => {
|
|
2775
|
+
const existingIds = new Set(existingItems.map(item => item.id));
|
|
2776
|
+
let occurrenceIndex = 0;
|
|
2777
|
+
while (existingIds.has(makeInstanceId(fullPath, occurrenceIndex))) {
|
|
2778
|
+
occurrenceIndex += 1;
|
|
2779
|
+
}
|
|
2780
|
+
return makeInstanceId(fullPath, occurrenceIndex);
|
|
2781
|
+
};
|
|
2782
|
+
|
|
2610
2783
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2611
2784
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2612
2785
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2618,6 +2791,53 @@ const interpolateTitle = (template, params) => {
|
|
|
2618
2791
|
});
|
|
2619
2792
|
};
|
|
2620
2793
|
|
|
2794
|
+
// Resolves the final column display order for the results grid: dimensions,
|
|
2795
|
+
// computed dimensions, and metrics combined into a single list.
|
|
2796
|
+
//
|
|
2797
|
+
// `columnOrder` is the optional, explicitly-arranged order (raw entries
|
|
2798
|
+
// `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
|
|
2799
|
+
// Order tab. When present, its entries come first, in that order. Anything
|
|
2800
|
+
// not covered by it (new fields added since it was last touched, or when
|
|
2801
|
+
// it's empty/undefined because no custom order has ever been defined) is
|
|
2802
|
+
// appended in the pre-existing default order: dimensions/computed (in their
|
|
2803
|
+
// own `dimensionOrder`) first, then metrics (in their own array order) —
|
|
2804
|
+
// this keeps behavior unchanged for reports that never define a column order.
|
|
2805
|
+
const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
2806
|
+
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2807
|
+
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2808
|
+
// `.key` regardless of where the item came from.
|
|
2809
|
+
// Keyed by instance id (not fullPath) so that two instances of the same
|
|
2810
|
+
// dimension/metric - added deliberately as duplicates - are treated as
|
|
2811
|
+
// distinct columns rather than collapsing into one.
|
|
2812
|
+
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2813
|
+
kind: item.kind,
|
|
2814
|
+
key: item.kind === 'computed' ? item.data.name : item.data.id,
|
|
2815
|
+
data: item.data
|
|
2816
|
+
}));
|
|
2817
|
+
const normalizedMetricItems = metrics.map(m => ({
|
|
2818
|
+
kind: 'metric',
|
|
2819
|
+
key: m.id,
|
|
2820
|
+
data: m
|
|
2821
|
+
}));
|
|
2822
|
+
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
2823
|
+
const resolved = [];
|
|
2824
|
+
const seen = new Set();
|
|
2825
|
+
(columnOrder || []).forEach(entry => {
|
|
2826
|
+
const seenKey = `${entry.type}:${entry.key}`;
|
|
2827
|
+
if (seen.has(seenKey) || !byKey[seenKey]) return;
|
|
2828
|
+
resolved.push(byKey[seenKey]);
|
|
2829
|
+
seen.add(seenKey);
|
|
2830
|
+
});
|
|
2831
|
+
[...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
|
|
2832
|
+
const seenKey = `${item.kind}:${item.key}`;
|
|
2833
|
+
if (!seen.has(seenKey)) {
|
|
2834
|
+
resolved.push(item);
|
|
2835
|
+
seen.add(seenKey);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
return resolved;
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2621
2841
|
// Sortable Chip Component
|
|
2622
2842
|
const SortableChip$1 = ({
|
|
2623
2843
|
id,
|
|
@@ -2630,7 +2850,7 @@ const SortableChip$1 = ({
|
|
|
2630
2850
|
isLast,
|
|
2631
2851
|
sortOrder,
|
|
2632
2852
|
onSortOrderChange,
|
|
2633
|
-
|
|
2853
|
+
instanceId,
|
|
2634
2854
|
defaultTitle,
|
|
2635
2855
|
customTitle,
|
|
2636
2856
|
onUpdateTitle,
|
|
@@ -2646,20 +2866,8 @@ const SortableChip$1 = ({
|
|
|
2646
2866
|
attributes,
|
|
2647
2867
|
listeners,
|
|
2648
2868
|
setNodeRef,
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
isDragging
|
|
2652
|
-
} = sortable.useSortable({
|
|
2653
|
-
id
|
|
2654
|
-
});
|
|
2655
|
-
const style = {
|
|
2656
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
2657
|
-
transition,
|
|
2658
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2659
|
-
display: "flex",
|
|
2660
|
-
alignItems: "center",
|
|
2661
|
-
width: "100%"
|
|
2662
|
-
};
|
|
2869
|
+
style
|
|
2870
|
+
} = useSortableRow(id);
|
|
2663
2871
|
|
|
2664
2872
|
// Focus input when entering edit mode
|
|
2665
2873
|
React.useEffect(() => {
|
|
@@ -2725,7 +2933,7 @@ const SortableChip$1 = ({
|
|
|
2725
2933
|
handleCancel();
|
|
2726
2934
|
return;
|
|
2727
2935
|
}
|
|
2728
|
-
onUpdateTitle(
|
|
2936
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
2729
2937
|
setIsEditing(false);
|
|
2730
2938
|
};
|
|
2731
2939
|
const handleCancel = () => {
|
|
@@ -2733,7 +2941,7 @@ const SortableChip$1 = ({
|
|
|
2733
2941
|
setEditValue("");
|
|
2734
2942
|
};
|
|
2735
2943
|
const handleReset = () => {
|
|
2736
|
-
onResetTitle(
|
|
2944
|
+
onResetTitle(instanceId);
|
|
2737
2945
|
setIsEditing(false);
|
|
2738
2946
|
setEditValue("");
|
|
2739
2947
|
};
|
|
@@ -2768,21 +2976,9 @@ const SortableChip$1 = ({
|
|
|
2768
2976
|
opacity: 1 // show icons on hover
|
|
2769
2977
|
}
|
|
2770
2978
|
}
|
|
2771
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
alignItems: "center",
|
|
2775
|
-
cursor: "grab",
|
|
2776
|
-
"&:active": {
|
|
2777
|
-
cursor: "grabbing"
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
2781
|
-
sx: {
|
|
2782
|
-
cursor: "grab",
|
|
2783
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2784
|
-
}
|
|
2785
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2979
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
2980
|
+
listeners: listeners
|
|
2981
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2786
2982
|
sx: {
|
|
2787
2983
|
minWidth: 0
|
|
2788
2984
|
}
|
|
@@ -2845,29 +3041,12 @@ const SortableChip$1 = ({
|
|
|
2845
3041
|
sx: {
|
|
2846
3042
|
flex: 1
|
|
2847
3043
|
}
|
|
2848
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
transition: "opacity 0.2s"
|
|
2855
|
-
}
|
|
2856
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2857
|
-
size: "small",
|
|
2858
|
-
onClick: onMoveUp,
|
|
2859
|
-
disabled: isFirst,
|
|
2860
|
-
"aria-label": "move up"
|
|
2861
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
2862
|
-
fontSize: "small"
|
|
2863
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2864
|
-
size: "small",
|
|
2865
|
-
onClick: onMoveDown,
|
|
2866
|
-
disabled: isLast,
|
|
2867
|
-
"aria-label": "move down"
|
|
2868
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
2869
|
-
fontSize: "small"
|
|
2870
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3044
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
3045
|
+
onMoveUp: onMoveUp,
|
|
3046
|
+
onMoveDown: onMoveDown,
|
|
3047
|
+
isFirst: isFirst,
|
|
3048
|
+
isLast: isLast
|
|
3049
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
2871
3050
|
inputRef: inputRef,
|
|
2872
3051
|
value: editValue,
|
|
2873
3052
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -2943,24 +3122,12 @@ const SortableComputedChip = ({
|
|
|
2943
3122
|
attributes,
|
|
2944
3123
|
listeners,
|
|
2945
3124
|
setNodeRef,
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
isDragging
|
|
2949
|
-
} = sortable.useSortable({
|
|
2950
|
-
id
|
|
2951
|
-
});
|
|
3125
|
+
style
|
|
3126
|
+
} = useSortableRow(id);
|
|
2952
3127
|
const [isEditing, setIsEditing] = React.useState(false);
|
|
2953
3128
|
const [editName, setEditName] = React.useState("");
|
|
2954
3129
|
const [editValue, setEditValue] = React.useState("");
|
|
2955
3130
|
const containerRef = React.useRef(null);
|
|
2956
|
-
const style = {
|
|
2957
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
2958
|
-
transition,
|
|
2959
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2960
|
-
display: "flex",
|
|
2961
|
-
alignItems: "center",
|
|
2962
|
-
width: "100%"
|
|
2963
|
-
};
|
|
2964
3131
|
|
|
2965
3132
|
// Handle click outside to cancel
|
|
2966
3133
|
React.useEffect(() => {
|
|
@@ -2985,7 +3152,7 @@ const SortableComputedChip = ({
|
|
|
2985
3152
|
const handleSave = () => {
|
|
2986
3153
|
const trimmedName = editName.trim();
|
|
2987
3154
|
const trimmedValue = editValue.trim();
|
|
2988
|
-
if (!trimmedName
|
|
3155
|
+
if (!trimmedName) return;
|
|
2989
3156
|
if (isNameTaken(trimmedName, name)) return;
|
|
2990
3157
|
onUpdate(name, {
|
|
2991
3158
|
name: trimmedName,
|
|
@@ -3023,21 +3190,9 @@ const SortableComputedChip = ({
|
|
|
3023
3190
|
opacity: 1
|
|
3024
3191
|
}
|
|
3025
3192
|
}
|
|
3026
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
alignItems: "center",
|
|
3030
|
-
cursor: "grab",
|
|
3031
|
-
"&:active": {
|
|
3032
|
-
cursor: "grabbing"
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
3036
|
-
sx: {
|
|
3037
|
-
cursor: "grab",
|
|
3038
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3039
|
-
}
|
|
3040
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
3193
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
3194
|
+
listeners: listeners
|
|
3195
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
3041
3196
|
icon: /*#__PURE__*/React__namespace.default.createElement(FunctionsIcon__default.default, {
|
|
3042
3197
|
sx: {
|
|
3043
3198
|
fontSize: "15px !important"
|
|
@@ -3101,29 +3256,12 @@ const SortableComputedChip = ({
|
|
|
3101
3256
|
sx: {
|
|
3102
3257
|
flex: 1
|
|
3103
3258
|
}
|
|
3104
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
transition: "opacity 0.2s"
|
|
3111
|
-
}
|
|
3112
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3113
|
-
size: "small",
|
|
3114
|
-
onClick: onMoveUp,
|
|
3115
|
-
disabled: isFirst,
|
|
3116
|
-
"aria-label": "move up"
|
|
3117
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
3118
|
-
fontSize: "small"
|
|
3119
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3120
|
-
size: "small",
|
|
3121
|
-
onClick: onMoveDown,
|
|
3122
|
-
disabled: isLast,
|
|
3123
|
-
"aria-label": "move down"
|
|
3124
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
3125
|
-
fontSize: "small"
|
|
3126
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3259
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
3260
|
+
onMoveUp: onMoveUp,
|
|
3261
|
+
onMoveDown: onMoveDown,
|
|
3262
|
+
isFirst: isFirst,
|
|
3263
|
+
isLast: isLast
|
|
3264
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3127
3265
|
value: editName,
|
|
3128
3266
|
onChange: e => setEditName(e.target.value),
|
|
3129
3267
|
onKeyDown: handleKeyDown,
|
|
@@ -3164,7 +3302,7 @@ const SortableComputedChip = ({
|
|
|
3164
3302
|
onClick: handleSave,
|
|
3165
3303
|
color: "primary",
|
|
3166
3304
|
"aria-label": "save computed dimension",
|
|
3167
|
-
disabled: !editName.trim() ||
|
|
3305
|
+
disabled: !editName.trim() || nameError
|
|
3168
3306
|
}, /*#__PURE__*/React__namespace.default.createElement(CheckIcon__default.default, {
|
|
3169
3307
|
fontSize: "small"
|
|
3170
3308
|
})))), /*#__PURE__*/React__namespace.default.createElement(material.Tooltip, {
|
|
@@ -3208,55 +3346,39 @@ const Dimensions = ({
|
|
|
3208
3346
|
const [computedValue, setComputedValue] = React.useState("");
|
|
3209
3347
|
|
|
3210
3348
|
// Setup drag and drop sensors
|
|
3211
|
-
const sensors =
|
|
3212
|
-
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
3213
|
-
}));
|
|
3349
|
+
const sensors = useSortableListSensors();
|
|
3214
3350
|
|
|
3215
3351
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3216
3352
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3217
3353
|
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3218
|
-
|
|
3354
|
+
// Keyed by instance id rather than fullPath, so two instances of the same
|
|
3355
|
+
// dimension (added deliberately as a duplicate) stay distinct entries.
|
|
3356
|
+
const dimensionById = Object.fromEntries(savedDimensions.map(d => [d.id, d]));
|
|
3219
3357
|
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3220
3358
|
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3221
3359
|
kind: "dimension",
|
|
3222
3360
|
key: entry.key,
|
|
3223
|
-
data:
|
|
3361
|
+
data: dimensionById[entry.key]
|
|
3224
3362
|
} : {
|
|
3225
3363
|
kind: "computed",
|
|
3226
3364
|
key: entry.key,
|
|
3227
3365
|
data: computedDimensionByName[entry.key]
|
|
3228
3366
|
}).filter(item => item.data);
|
|
3229
3367
|
|
|
3230
|
-
// Handle drag
|
|
3231
|
-
const
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
if (over && active.id !== over.id) {
|
|
3237
|
-
const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
|
|
3238
|
-
const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
|
|
3239
|
-
onReorderDimensionItems(sortable.arrayMove(dimensionOrder, oldIndex, newIndex));
|
|
3240
|
-
}
|
|
3241
|
-
};
|
|
3242
|
-
|
|
3243
|
-
// Handle move up
|
|
3244
|
-
const handleMoveUp = index => {
|
|
3245
|
-
if (index > 0) {
|
|
3246
|
-
onReorderDimensionItems(sortable.arrayMove(dimensionOrder, index, index - 1));
|
|
3247
|
-
}
|
|
3248
|
-
};
|
|
3249
|
-
|
|
3250
|
-
// Handle move down
|
|
3251
|
-
const handleMoveDown = index => {
|
|
3252
|
-
if (index < dimensionOrder.length - 1) {
|
|
3253
|
-
onReorderDimensionItems(sortable.arrayMove(dimensionOrder, index, index + 1));
|
|
3254
|
-
}
|
|
3255
|
-
};
|
|
3368
|
+
// Handle drag/move for the unified list
|
|
3369
|
+
const {
|
|
3370
|
+
handleDragEnd,
|
|
3371
|
+
handleMoveUp,
|
|
3372
|
+
handleMoveDown
|
|
3373
|
+
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3256
3374
|
|
|
3257
|
-
// Handle sort order change (dimensions only - computed dimensions have no
|
|
3258
|
-
|
|
3259
|
-
|
|
3375
|
+
// Handle sort order change (dimensions only - computed dimensions have no
|
|
3376
|
+
// order_by). Keyed by instance id but applied query-wide: SQL can only
|
|
3377
|
+
// sort by a given column once, so toggling sort on any duplicate instance
|
|
3378
|
+
// of the same field is propagated to all instances sharing its fullPath
|
|
3379
|
+
// (handled in ReportBuilder's onUpdateDimensionSortOrder).
|
|
3380
|
+
const handleSortOrderChange = (id, sortOrder) => {
|
|
3381
|
+
onUpdateDimensionSortOrder(id, sortOrder);
|
|
3260
3382
|
};
|
|
3261
3383
|
|
|
3262
3384
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
@@ -3277,7 +3399,7 @@ const Dimensions = ({
|
|
|
3277
3399
|
const handleSaveComputed = () => {
|
|
3278
3400
|
const trimmedName = computedName.trim();
|
|
3279
3401
|
const trimmedValue = computedValue.trim();
|
|
3280
|
-
if (!trimmedName
|
|
3402
|
+
if (!trimmedName) return;
|
|
3281
3403
|
if (isComputedNameTaken(trimmedName)) return;
|
|
3282
3404
|
onSaveComputedDimension({
|
|
3283
3405
|
name: trimmedName,
|
|
@@ -3319,7 +3441,10 @@ const Dimensions = ({
|
|
|
3319
3441
|
currentProviderKey = selection.targetKey;
|
|
3320
3442
|
});
|
|
3321
3443
|
|
|
3322
|
-
// Create a Set of already selected dimension fullPaths for quick lookup
|
|
3444
|
+
// Create a Set of already selected dimension fullPaths for quick lookup.
|
|
3445
|
+
// Already-selected items stay selectable (so the same dimension can be
|
|
3446
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
3447
|
+
// highlight them instead of blocking the click.
|
|
3323
3448
|
const selectedFullPaths = new Set(savedDimensions.map(dim => dim.fullPath));
|
|
3324
3449
|
const items = [];
|
|
3325
3450
|
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
@@ -3331,9 +3456,6 @@ const Dimensions = ({
|
|
|
3331
3456
|
|
|
3332
3457
|
// Construct the fullPath for this dimension
|
|
3333
3458
|
const fullPath = `${aliasPath.join("_")}.${dimKey}`;
|
|
3334
|
-
|
|
3335
|
-
// Check if this dimension is already selected
|
|
3336
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
3337
3459
|
items.push({
|
|
3338
3460
|
// Include provider name to ensure uniqueness across different providers
|
|
3339
3461
|
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
@@ -3341,7 +3463,7 @@ const Dimensions = ({
|
|
|
3341
3463
|
dimensionKey: dimKey,
|
|
3342
3464
|
alias: alias,
|
|
3343
3465
|
dimension: dimension,
|
|
3344
|
-
|
|
3466
|
+
alreadySelected: selectedFullPaths.has(fullPath)
|
|
3345
3467
|
});
|
|
3346
3468
|
});
|
|
3347
3469
|
});
|
|
@@ -3403,7 +3525,10 @@ const Dimensions = ({
|
|
|
3403
3525
|
dimensionKey: selectedItem.dimensionKey,
|
|
3404
3526
|
dimensionTitle: selectedItem.value,
|
|
3405
3527
|
// The constructed path for server
|
|
3406
|
-
fullPath: fullPath
|
|
3528
|
+
fullPath: fullPath,
|
|
3529
|
+
// Unique per-instance identity, so this field can be added more than
|
|
3530
|
+
// once and each occurrence stays independently orderable/removable.
|
|
3531
|
+
id: nextInstanceId(fullPath, savedDimensions)
|
|
3407
3532
|
};
|
|
3408
3533
|
onSaveDimension(dimensionData);
|
|
3409
3534
|
handleCancel();
|
|
@@ -3414,8 +3539,9 @@ const Dimensions = ({
|
|
|
3414
3539
|
const handleAddAll = () => {
|
|
3415
3540
|
const dimensionItems = getDimensionItems();
|
|
3416
3541
|
|
|
3417
|
-
// Filter out already selected dimensions
|
|
3418
|
-
|
|
3542
|
+
// Filter out already selected dimensions - "Add all" only fills in
|
|
3543
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
3544
|
+
const availableItems = dimensionItems.filter(item => !item.alreadySelected);
|
|
3419
3545
|
if (availableItems.length === 0) return;
|
|
3420
3546
|
|
|
3421
3547
|
// Build the complete relation objects array (same for all dimensions in this provider)
|
|
@@ -3460,7 +3586,10 @@ const Dimensions = ({
|
|
|
3460
3586
|
dimensionKey: item.dimensionKey,
|
|
3461
3587
|
dimensionTitle: item.value,
|
|
3462
3588
|
// The constructed path for server
|
|
3463
|
-
fullPath: fullPath
|
|
3589
|
+
fullPath: fullPath,
|
|
3590
|
+
// "Add all" only ever adds fields that aren't already selected, so
|
|
3591
|
+
// this is always the first (and only) occurrence of this fullPath.
|
|
3592
|
+
id: fullPath
|
|
3464
3593
|
};
|
|
3465
3594
|
onSaveDimension(dimensionData);
|
|
3466
3595
|
});
|
|
@@ -3594,7 +3723,7 @@ const Dimensions = ({
|
|
|
3594
3723
|
}, "Cancel"), /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
3595
3724
|
variant: "contained",
|
|
3596
3725
|
onClick: handleSaveComputed,
|
|
3597
|
-
disabled: !computedName.trim() ||
|
|
3726
|
+
disabled: !computedName.trim() || computedNameError,
|
|
3598
3727
|
sx: {
|
|
3599
3728
|
height: "40px",
|
|
3600
3729
|
fontFamily: "system-ui",
|
|
@@ -3670,7 +3799,7 @@ const Dimensions = ({
|
|
|
3670
3799
|
placement: "top"
|
|
3671
3800
|
}, /*#__PURE__*/React__namespace.default.createElement("span", null, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3672
3801
|
onClick: handleAddAll,
|
|
3673
|
-
disabled: getDimensionItems().filter(item => !item.
|
|
3802
|
+
disabled: getDimensionItems().filter(item => !item.alreadySelected).length === 0,
|
|
3674
3803
|
"aria-label": "add all dimensions",
|
|
3675
3804
|
sx: {
|
|
3676
3805
|
color: "rgb(70, 134, 128)",
|
|
@@ -3745,19 +3874,10 @@ const Dimensions = ({
|
|
|
3745
3874
|
color: "#666",
|
|
3746
3875
|
marginBottom: 2
|
|
3747
3876
|
}
|
|
3748
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(
|
|
3877
|
+
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
3749
3878
|
sensors: sensors,
|
|
3750
|
-
|
|
3879
|
+
itemIds: orderedItems.map((_, index) => index),
|
|
3751
3880
|
onDragEnd: handleDragEnd
|
|
3752
|
-
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
3753
|
-
items: orderedItems.map((_, index) => index),
|
|
3754
|
-
strategy: sortable.verticalListSortingStrategy
|
|
3755
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3756
|
-
sx: {
|
|
3757
|
-
display: "flex",
|
|
3758
|
-
flexDirection: "column",
|
|
3759
|
-
gap: 1
|
|
3760
|
-
}
|
|
3761
3881
|
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__namespace.default.createElement(SortableChip$1, {
|
|
3762
3882
|
key: `dim-${item.key}`,
|
|
3763
3883
|
id: index,
|
|
@@ -3770,9 +3890,9 @@ const Dimensions = ({
|
|
|
3770
3890
|
isLast: index === orderedItems.length - 1,
|
|
3771
3891
|
sortOrder: item.data.sortOrder || null,
|
|
3772
3892
|
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3773
|
-
|
|
3893
|
+
instanceId: item.data.id,
|
|
3774
3894
|
defaultTitle: item.data.dimensionTitle,
|
|
3775
|
-
customTitle: titleOverrides[item.data.
|
|
3895
|
+
customTitle: titleOverrides[item.data.id],
|
|
3776
3896
|
onUpdateTitle: onUpdateTitle,
|
|
3777
3897
|
onResetTitle: onResetTitle
|
|
3778
3898
|
}) : /*#__PURE__*/React__namespace.default.createElement(SortableComputedChip, {
|
|
@@ -3787,7 +3907,7 @@ const Dimensions = ({
|
|
|
3787
3907
|
onMoveDown: () => handleMoveDown(index),
|
|
3788
3908
|
isFirst: index === 0,
|
|
3789
3909
|
isLast: index === orderedItems.length - 1
|
|
3790
|
-
})))))
|
|
3910
|
+
})))));
|
|
3791
3911
|
};
|
|
3792
3912
|
|
|
3793
3913
|
const MetricSourceIcon = ({
|
|
@@ -3845,7 +3965,7 @@ const SortableChip = ({
|
|
|
3845
3965
|
onMoveDown,
|
|
3846
3966
|
isFirst,
|
|
3847
3967
|
isLast,
|
|
3848
|
-
|
|
3968
|
+
instanceId,
|
|
3849
3969
|
defaultTitle,
|
|
3850
3970
|
customTitle,
|
|
3851
3971
|
onUpdateTitle,
|
|
@@ -3862,20 +3982,8 @@ const SortableChip = ({
|
|
|
3862
3982
|
attributes,
|
|
3863
3983
|
listeners,
|
|
3864
3984
|
setNodeRef,
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
isDragging
|
|
3868
|
-
} = sortable.useSortable({
|
|
3869
|
-
id
|
|
3870
|
-
});
|
|
3871
|
-
const style = {
|
|
3872
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
3873
|
-
transition,
|
|
3874
|
-
opacity: isDragging ? 0.5 : 1,
|
|
3875
|
-
display: 'flex',
|
|
3876
|
-
alignItems: 'center',
|
|
3877
|
-
width: '100%'
|
|
3878
|
-
};
|
|
3985
|
+
style
|
|
3986
|
+
} = useSortableRow(id);
|
|
3879
3987
|
|
|
3880
3988
|
// Focus input when entering edit mode
|
|
3881
3989
|
React.useEffect(() => {
|
|
@@ -3910,7 +4018,7 @@ const SortableChip = ({
|
|
|
3910
4018
|
handleCancel();
|
|
3911
4019
|
return;
|
|
3912
4020
|
}
|
|
3913
|
-
onUpdateTitle(
|
|
4021
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
3914
4022
|
setIsEditing(false);
|
|
3915
4023
|
};
|
|
3916
4024
|
const handleCancel = () => {
|
|
@@ -3918,7 +4026,7 @@ const SortableChip = ({
|
|
|
3918
4026
|
setEditValue('');
|
|
3919
4027
|
};
|
|
3920
4028
|
const handleReset = () => {
|
|
3921
|
-
onResetTitle(
|
|
4029
|
+
onResetTitle(instanceId);
|
|
3922
4030
|
setIsEditing(false);
|
|
3923
4031
|
setEditValue('');
|
|
3924
4032
|
};
|
|
@@ -3953,21 +4061,9 @@ const SortableChip = ({
|
|
|
3953
4061
|
opacity: 1 // show icons on hover
|
|
3954
4062
|
}
|
|
3955
4063
|
}
|
|
3956
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
alignItems: "center",
|
|
3960
|
-
cursor: "grab",
|
|
3961
|
-
"&:active": {
|
|
3962
|
-
cursor: "grabbing"
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
3966
|
-
sx: {
|
|
3967
|
-
cursor: "grab",
|
|
3968
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3969
|
-
}
|
|
3970
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, source && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4064
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
4065
|
+
listeners: listeners
|
|
4066
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, source && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3971
4067
|
sx: {
|
|
3972
4068
|
display: 'flex',
|
|
3973
4069
|
alignItems: 'center',
|
|
@@ -4029,29 +4125,12 @@ const SortableChip = ({
|
|
|
4029
4125
|
sx: {
|
|
4030
4126
|
flex: 1
|
|
4031
4127
|
}
|
|
4032
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
transition: "opacity 0.2s"
|
|
4039
|
-
}
|
|
4040
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4041
|
-
size: "small",
|
|
4042
|
-
onClick: onMoveUp,
|
|
4043
|
-
disabled: isFirst,
|
|
4044
|
-
"aria-label": "move up"
|
|
4045
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
4046
|
-
fontSize: "small"
|
|
4047
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4048
|
-
size: "small",
|
|
4049
|
-
onClick: onMoveDown,
|
|
4050
|
-
disabled: isLast,
|
|
4051
|
-
"aria-label": "move down"
|
|
4052
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
4053
|
-
fontSize: "small"
|
|
4054
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4128
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
4129
|
+
onMoveUp: onMoveUp,
|
|
4130
|
+
onMoveDown: onMoveDown,
|
|
4131
|
+
isFirst: isFirst,
|
|
4132
|
+
isLast: isLast
|
|
4133
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4055
4134
|
inputRef: inputRef,
|
|
4056
4135
|
value: editValue,
|
|
4057
4136
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -4126,39 +4205,14 @@ const Metrics = ({
|
|
|
4126
4205
|
const [selectedMetric, setSelectedMetric] = React.useState('');
|
|
4127
4206
|
|
|
4128
4207
|
// Setup drag and drop sensors
|
|
4129
|
-
const sensors =
|
|
4130
|
-
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
4131
|
-
}));
|
|
4132
|
-
|
|
4133
|
-
// Handle drag end
|
|
4134
|
-
const handleDragEnd = event => {
|
|
4135
|
-
const {
|
|
4136
|
-
active,
|
|
4137
|
-
over
|
|
4138
|
-
} = event;
|
|
4139
|
-
if (over && active.id !== over.id) {
|
|
4140
|
-
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
4141
|
-
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
4142
|
-
const newOrder = sortable.arrayMove(savedMetrics, oldIndex, newIndex);
|
|
4143
|
-
onReorderMetrics(newOrder);
|
|
4144
|
-
}
|
|
4145
|
-
};
|
|
4208
|
+
const sensors = useSortableListSensors();
|
|
4146
4209
|
|
|
4147
|
-
// Handle move
|
|
4148
|
-
const
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
};
|
|
4154
|
-
|
|
4155
|
-
// Handle move down
|
|
4156
|
-
const handleMoveDown = index => {
|
|
4157
|
-
if (index < savedMetrics.length - 1) {
|
|
4158
|
-
const newOrder = sortable.arrayMove(savedMetrics, index, index + 1);
|
|
4159
|
-
onReorderMetrics(newOrder);
|
|
4160
|
-
}
|
|
4161
|
-
};
|
|
4210
|
+
// Handle drag/move
|
|
4211
|
+
const {
|
|
4212
|
+
handleDragEnd,
|
|
4213
|
+
handleMoveUp,
|
|
4214
|
+
handleMoveDown
|
|
4215
|
+
} = makeListReorderHandlers(savedMetrics, onReorderMetrics);
|
|
4162
4216
|
|
|
4163
4217
|
// Get the current provider based on selection chain
|
|
4164
4218
|
const getCurrentProvider = () => {
|
|
@@ -4193,7 +4247,10 @@ const Metrics = ({
|
|
|
4193
4247
|
currentProviderKey = selection.targetKey;
|
|
4194
4248
|
});
|
|
4195
4249
|
|
|
4196
|
-
// Create a Set of already selected metric fullPaths for quick lookup
|
|
4250
|
+
// Create a Set of already selected metric fullPaths for quick lookup.
|
|
4251
|
+
// Already-selected items stay selectable (so the same metric can be
|
|
4252
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
4253
|
+
// highlight them instead of blocking the click.
|
|
4197
4254
|
const selectedFullPaths = new Set(savedMetrics.map(metric => metric.fullPath));
|
|
4198
4255
|
|
|
4199
4256
|
// Metrics are a direct array, not nested by alias
|
|
@@ -4201,15 +4258,12 @@ const Metrics = ({
|
|
|
4201
4258
|
const items = provider.metrics.map((metric, index) => {
|
|
4202
4259
|
// Construct the fullPath for this metric
|
|
4203
4260
|
const fullPath = `${aliasPath.join('_')}.${metric.name}`;
|
|
4204
|
-
|
|
4205
|
-
// Check if this metric is already selected
|
|
4206
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
4207
4261
|
return {
|
|
4208
4262
|
key: `${currentProvider}_${metric.name}_${index}`,
|
|
4209
4263
|
value: metric.title || metric.name,
|
|
4210
4264
|
metricName: metric.name,
|
|
4211
4265
|
metric: metric,
|
|
4212
|
-
|
|
4266
|
+
alreadySelected: selectedFullPaths.has(fullPath),
|
|
4213
4267
|
icon: metric.source ? /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
4214
4268
|
source: metric.source
|
|
4215
4269
|
}) : undefined
|
|
@@ -4272,7 +4326,11 @@ const Metrics = ({
|
|
|
4272
4326
|
metricName: selectedItem.metricName,
|
|
4273
4327
|
metricTitle: selectedItem.value,
|
|
4274
4328
|
// The constructed path for server
|
|
4275
|
-
fullPath: fullPath
|
|
4329
|
+
fullPath: fullPath,
|
|
4330
|
+
// Unique per-instance identity, so this field can be added more
|
|
4331
|
+
// than once and each occurrence stays independently
|
|
4332
|
+
// orderable/removable/titleable.
|
|
4333
|
+
id: nextInstanceId(fullPath, savedMetrics)
|
|
4276
4334
|
};
|
|
4277
4335
|
onSaveMetric(metricData);
|
|
4278
4336
|
handleCancel();
|
|
@@ -4283,8 +4341,9 @@ const Metrics = ({
|
|
|
4283
4341
|
const handleAddAll = () => {
|
|
4284
4342
|
const metricItems = getMetricItems();
|
|
4285
4343
|
|
|
4286
|
-
// Filter out already selected metrics
|
|
4287
|
-
|
|
4344
|
+
// Filter out already selected metrics - "Add all" only fills in
|
|
4345
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
4346
|
+
const availableItems = metricItems.filter(item => !item.alreadySelected);
|
|
4288
4347
|
if (availableItems.length === 0) return;
|
|
4289
4348
|
|
|
4290
4349
|
// Build the complete relation objects array (same for all metrics in this provider)
|
|
@@ -4329,7 +4388,10 @@ const Metrics = ({
|
|
|
4329
4388
|
metricName: item.metricName,
|
|
4330
4389
|
metricTitle: item.value,
|
|
4331
4390
|
// The constructed path for server
|
|
4332
|
-
fullPath: fullPath
|
|
4391
|
+
fullPath: fullPath,
|
|
4392
|
+
// "Add all" only ever adds fields that aren't already
|
|
4393
|
+
// selected, so this is always the first occurrence.
|
|
4394
|
+
id: fullPath
|
|
4333
4395
|
};
|
|
4334
4396
|
onSaveMetric(metricData);
|
|
4335
4397
|
});
|
|
@@ -4428,7 +4490,7 @@ const Metrics = ({
|
|
|
4428
4490
|
placement: "top"
|
|
4429
4491
|
}, /*#__PURE__*/React__namespace.default.createElement("span", null, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4430
4492
|
onClick: handleAddAll,
|
|
4431
|
-
disabled: getMetricItems().filter(item => !item.
|
|
4493
|
+
disabled: getMetricItems().filter(item => !item.alreadySelected).length === 0,
|
|
4432
4494
|
"aria-label": "add all metrics",
|
|
4433
4495
|
sx: {
|
|
4434
4496
|
color: "rgb(70, 134, 128)",
|
|
@@ -4497,36 +4559,27 @@ const Metrics = ({
|
|
|
4497
4559
|
marginBottom: 1,
|
|
4498
4560
|
color: "rgb(37, 37, 37)"
|
|
4499
4561
|
}
|
|
4500
|
-
}, "Saved Metrics"), /*#__PURE__*/React__namespace.default.createElement(
|
|
4562
|
+
}, "Saved Metrics"), /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
4501
4563
|
sensors: sensors,
|
|
4502
|
-
|
|
4564
|
+
itemIds: savedMetrics.map((_, index) => index),
|
|
4503
4565
|
onDragEnd: handleDragEnd
|
|
4504
|
-
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
4505
|
-
items: savedMetrics.map((_, index) => index),
|
|
4506
|
-
strategy: sortable.verticalListSortingStrategy
|
|
4507
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4508
|
-
sx: {
|
|
4509
|
-
display: 'flex',
|
|
4510
|
-
flexDirection: 'column',
|
|
4511
|
-
gap: 1
|
|
4512
|
-
}
|
|
4513
4566
|
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__namespace.default.createElement(SortableChip, {
|
|
4514
4567
|
key: index,
|
|
4515
4568
|
id: index,
|
|
4516
4569
|
label: metric.metricTitle,
|
|
4517
4570
|
fullLabel: `${formatProviderPath(metric)} → ${metric.metricTitle} (${metric.fullPath})`,
|
|
4518
|
-
onDelete: () => onRemoveMetric(
|
|
4571
|
+
onDelete: () => onRemoveMetric(metric.id),
|
|
4519
4572
|
onMoveUp: () => handleMoveUp(index),
|
|
4520
4573
|
onMoveDown: () => handleMoveDown(index),
|
|
4521
4574
|
isFirst: index === 0,
|
|
4522
4575
|
isLast: index === savedMetrics.length - 1,
|
|
4523
|
-
|
|
4576
|
+
instanceId: metric.id,
|
|
4524
4577
|
defaultTitle: metric.metricTitle,
|
|
4525
|
-
customTitle: titleOverrides[metric.
|
|
4578
|
+
customTitle: titleOverrides[metric.id],
|
|
4526
4579
|
onUpdateTitle: onUpdateTitle,
|
|
4527
4580
|
onResetTitle: onResetTitle,
|
|
4528
4581
|
source: metric.metric?.source
|
|
4529
|
-
})))))
|
|
4582
|
+
})))));
|
|
4530
4583
|
};
|
|
4531
4584
|
|
|
4532
4585
|
const Filters = ({
|
|
@@ -5359,7 +5412,12 @@ const Filters = ({
|
|
|
5359
5412
|
mb: 3
|
|
5360
5413
|
}
|
|
5361
5414
|
}, /*#__PURE__*/React__namespace.default.createElement(SingleSelect, {
|
|
5362
|
-
items:
|
|
5415
|
+
items: Array.from(
|
|
5416
|
+
// Dimensions can now be added more than once (same fullPath,
|
|
5417
|
+
// different instance), but a filter targets the underlying
|
|
5418
|
+
// field regardless of how many columns display it - so this
|
|
5419
|
+
// list is deduped by fullPath to avoid showing it twice.
|
|
5420
|
+
new Map(existingDimensions.map(dim => [dim.fullPath, dim])).values()).map(dim => ({
|
|
5363
5421
|
key: dim.fullPath,
|
|
5364
5422
|
value: interpolateTitle(dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle, parameters)
|
|
5365
5423
|
})).sort((a, b) => a.value.localeCompare(b.value)),
|
|
@@ -5753,6 +5811,205 @@ const Filters = ({
|
|
|
5753
5811
|
}))));
|
|
5754
5812
|
};
|
|
5755
5813
|
|
|
5814
|
+
const KIND_LABEL = {
|
|
5815
|
+
dimension: "Dimension",
|
|
5816
|
+
computed: "Computed",
|
|
5817
|
+
metric: "Metric"
|
|
5818
|
+
};
|
|
5819
|
+
const KindChip = ({
|
|
5820
|
+
kind,
|
|
5821
|
+
source
|
|
5822
|
+
}) => {
|
|
5823
|
+
if (kind === "metric") {
|
|
5824
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5825
|
+
sx: {
|
|
5826
|
+
display: "flex",
|
|
5827
|
+
alignItems: "center",
|
|
5828
|
+
flexShrink: 0
|
|
5829
|
+
}
|
|
5830
|
+
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
5831
|
+
source: source
|
|
5832
|
+
}));
|
|
5833
|
+
}
|
|
5834
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
5835
|
+
icon: kind === "computed" ? /*#__PURE__*/React__namespace.default.createElement(CalculateIcon__default.default, {
|
|
5836
|
+
sx: {
|
|
5837
|
+
fontSize: "15px !important"
|
|
5838
|
+
}
|
|
5839
|
+
}) : undefined,
|
|
5840
|
+
label: KIND_LABEL[kind],
|
|
5841
|
+
size: "small",
|
|
5842
|
+
sx: {
|
|
5843
|
+
fontWeight: 500,
|
|
5844
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
5845
|
+
color: "rgb(70, 134, 128)",
|
|
5846
|
+
flexShrink: 0
|
|
5847
|
+
}
|
|
5848
|
+
});
|
|
5849
|
+
};
|
|
5850
|
+
|
|
5851
|
+
// Read-only sortable row: this tab only reorders columns. Adding, removing,
|
|
5852
|
+
// and renaming stay on the Dimensions/Metrics tabs so there's one place each
|
|
5853
|
+
// field is managed.
|
|
5854
|
+
const SortableColumnRow = ({
|
|
5855
|
+
id,
|
|
5856
|
+
label,
|
|
5857
|
+
fullPath,
|
|
5858
|
+
kind,
|
|
5859
|
+
source,
|
|
5860
|
+
onMoveUp,
|
|
5861
|
+
onMoveDown,
|
|
5862
|
+
isFirst,
|
|
5863
|
+
isLast
|
|
5864
|
+
}) => {
|
|
5865
|
+
const reportingContext = useReportingContextOptional();
|
|
5866
|
+
const parameters = reportingContext?.parameters;
|
|
5867
|
+
const {
|
|
5868
|
+
attributes,
|
|
5869
|
+
listeners,
|
|
5870
|
+
setNodeRef,
|
|
5871
|
+
style
|
|
5872
|
+
} = useSortableRow(id);
|
|
5873
|
+
const displayLabel = interpolateTitle(label, parameters);
|
|
5874
|
+
return /*#__PURE__*/React__namespace.default.createElement("div", _extends({
|
|
5875
|
+
ref: setNodeRef,
|
|
5876
|
+
style: style
|
|
5877
|
+
}, attributes), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5878
|
+
sx: {
|
|
5879
|
+
display: "flex",
|
|
5880
|
+
alignItems: "center",
|
|
5881
|
+
width: "100%",
|
|
5882
|
+
gap: 1,
|
|
5883
|
+
backgroundColor: "white",
|
|
5884
|
+
border: "1px solid #e0e0e0",
|
|
5885
|
+
borderRadius: 2,
|
|
5886
|
+
padding: 1,
|
|
5887
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
5888
|
+
"&:hover .hover-icons": {
|
|
5889
|
+
opacity: 1
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
5893
|
+
listeners: listeners
|
|
5894
|
+
}), /*#__PURE__*/React__namespace.default.createElement(KindChip, {
|
|
5895
|
+
kind: kind,
|
|
5896
|
+
source: source
|
|
5897
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Tooltip, {
|
|
5898
|
+
title: fullPath,
|
|
5899
|
+
arrow: true,
|
|
5900
|
+
placement: "top"
|
|
5901
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5902
|
+
variant: "h6",
|
|
5903
|
+
sx: {
|
|
5904
|
+
fontWeight: 500,
|
|
5905
|
+
color: "#1a1a1a",
|
|
5906
|
+
fontSize: "14px",
|
|
5907
|
+
whiteSpace: "nowrap",
|
|
5908
|
+
overflow: "hidden",
|
|
5909
|
+
textOverflow: "ellipsis"
|
|
5910
|
+
}
|
|
5911
|
+
}, displayLabel)), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5912
|
+
sx: {
|
|
5913
|
+
flex: 1
|
|
5914
|
+
}
|
|
5915
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
5916
|
+
onMoveUp: onMoveUp,
|
|
5917
|
+
onMoveDown: onMoveDown,
|
|
5918
|
+
isFirst: isFirst,
|
|
5919
|
+
isLast: isLast
|
|
5920
|
+
})));
|
|
5921
|
+
};
|
|
5922
|
+
const ColumnOrder = ({
|
|
5923
|
+
orderedDimensionItems = [],
|
|
5924
|
+
metrics = [],
|
|
5925
|
+
columnOrder = [],
|
|
5926
|
+
onReorderColumnItems,
|
|
5927
|
+
titleOverrides = {
|
|
5928
|
+
dimensions: {},
|
|
5929
|
+
metrics: {}
|
|
5930
|
+
}
|
|
5931
|
+
}) => {
|
|
5932
|
+
const isCustomized = columnOrder.length > 0;
|
|
5933
|
+
const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
|
|
5934
|
+
const sensors = useSortableListSensors();
|
|
5935
|
+
const reorderResolved = newResolvedItems => {
|
|
5936
|
+
onReorderColumnItems(newResolvedItems.map(item => ({
|
|
5937
|
+
type: item.kind,
|
|
5938
|
+
key: item.key
|
|
5939
|
+
})));
|
|
5940
|
+
};
|
|
5941
|
+
const {
|
|
5942
|
+
handleDragEnd,
|
|
5943
|
+
handleMoveUp,
|
|
5944
|
+
handleMoveDown
|
|
5945
|
+
} = makeListReorderHandlers(resolvedItems, reorderResolved);
|
|
5946
|
+
const labelFor = item => {
|
|
5947
|
+
if (item.kind === "computed") return item.data.name;
|
|
5948
|
+
if (item.kind === "metric") {
|
|
5949
|
+
return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
|
|
5950
|
+
}
|
|
5951
|
+
return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
|
|
5952
|
+
};
|
|
5953
|
+
return /*#__PURE__*/React__namespace.default.createElement("div", null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5954
|
+
sx: {
|
|
5955
|
+
display: "flex",
|
|
5956
|
+
justifyContent: "space-between",
|
|
5957
|
+
alignItems: "flex-start",
|
|
5958
|
+
mb: 2,
|
|
5959
|
+
gap: 2
|
|
5960
|
+
}
|
|
5961
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5962
|
+
sx: {
|
|
5963
|
+
fontSize: "13px",
|
|
5964
|
+
color: "#666"
|
|
5965
|
+
}
|
|
5966
|
+
}, "Optional: define a single order for all columns (dimensions, computed dimensions and metrics together) in the results grid. Drag to reorder or use the arrows. Newly added dimensions/metrics are appended to the end automatically. Without a custom order, columns follow the Dimensions tab order, then the Metrics tab order."), isCustomized && /*#__PURE__*/React__namespace.default.createElement(material.Tooltip, {
|
|
5967
|
+
title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
|
|
5968
|
+
arrow: true
|
|
5969
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
5970
|
+
variant: "outlined",
|
|
5971
|
+
size: "small",
|
|
5972
|
+
startIcon: /*#__PURE__*/React__namespace.default.createElement(RestartAltIcon__default.default, null),
|
|
5973
|
+
onClick: () => onReorderColumnItems([]),
|
|
5974
|
+
sx: {
|
|
5975
|
+
height: "36px",
|
|
5976
|
+
flexShrink: 0,
|
|
5977
|
+
fontFamily: "system-ui",
|
|
5978
|
+
fontSize: "13px",
|
|
5979
|
+
fontWeight: 500,
|
|
5980
|
+
borderRadius: "8px",
|
|
5981
|
+
boxShadow: "none",
|
|
5982
|
+
textTransform: "none",
|
|
5983
|
+
borderColor: "#e0e0e0",
|
|
5984
|
+
color: "rgb(37, 37, 37)",
|
|
5985
|
+
"&:hover": {
|
|
5986
|
+
backgroundColor: "#f5f5f5",
|
|
5987
|
+
borderColor: "#d0d0d0"
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
}, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5991
|
+
sx: {
|
|
5992
|
+
fontSize: "13px",
|
|
5993
|
+
color: "#999"
|
|
5994
|
+
}
|
|
5995
|
+
}, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
5996
|
+
sensors: sensors,
|
|
5997
|
+
itemIds: resolvedItems.map((_, index) => index),
|
|
5998
|
+
onDragEnd: handleDragEnd
|
|
5999
|
+
}, resolvedItems.map((item, index) => /*#__PURE__*/React__namespace.default.createElement(SortableColumnRow, {
|
|
6000
|
+
key: `${item.kind}-${item.key}`,
|
|
6001
|
+
id: index,
|
|
6002
|
+
label: labelFor(item),
|
|
6003
|
+
fullPath: item.kind === "computed" ? item.data.value : item.data.fullPath,
|
|
6004
|
+
kind: item.kind,
|
|
6005
|
+
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
6006
|
+
onMoveUp: () => handleMoveUp(index),
|
|
6007
|
+
onMoveDown: () => handleMoveDown(index),
|
|
6008
|
+
isFirst: index === 0,
|
|
6009
|
+
isLast: index === resolvedItems.length - 1
|
|
6010
|
+
}))));
|
|
6011
|
+
};
|
|
6012
|
+
|
|
5756
6013
|
// Default numeral.js formats applied when a dimension/metric definition has a
|
|
5757
6014
|
// known type but no explicit `format` set on the provider.
|
|
5758
6015
|
const DEFAULT_FORMATS_BY_TYPE = {
|
|
@@ -5767,10 +6024,122 @@ const formatValue = (value, def) => {
|
|
|
5767
6024
|
return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
|
|
5768
6025
|
};
|
|
5769
6026
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
6027
|
+
|
|
6028
|
+
// Builds a single DataGrid column definition for one resolved column item
|
|
6029
|
+
// (a dimension, computed dimension, or metric). The DataGrid `field` is the
|
|
6030
|
+
// item's instance id (so two duplicate instances of the same underlying
|
|
6031
|
+
// dimension/metric still get distinct columns); the actual cell value is
|
|
6032
|
+
// read via `valueGetter` from `dataFieldName`, the key the API response
|
|
6033
|
+
// actually uses - shared by every duplicate instance, since the backend
|
|
6034
|
+
// only ever returns one value per fullPath. dataFieldName rules:
|
|
6035
|
+
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
6036
|
+
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
6037
|
+
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
6038
|
+
// each row keyed by the computed dimension's own `name` (and can't be
|
|
6039
|
+
// duplicated - names must be unique - so field/dataFieldName coincide).
|
|
6040
|
+
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
6041
|
+
// (see comment inline below), normalised to match the row-key normalisation
|
|
6042
|
+
// done when building `rows`.
|
|
6043
|
+
const buildColumn = (item, titleOverrides, parameters) => {
|
|
6044
|
+
if (item.kind === 'computed') {
|
|
6045
|
+
const cd = item.data;
|
|
6046
|
+
return {
|
|
6047
|
+
field: cd.name,
|
|
6048
|
+
headerName: cd.name,
|
|
6049
|
+
flex: 1,
|
|
6050
|
+
minWidth: 150,
|
|
6051
|
+
type: 'string'
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
if (item.kind === 'metric') {
|
|
6055
|
+
const metric = item.data;
|
|
6056
|
+
const metricDef = metric.metric;
|
|
6057
|
+
let dataFieldName;
|
|
6058
|
+
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
6059
|
+
if (dotCount >= 2) {
|
|
6060
|
+
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
6061
|
+
// Normalise dots to underscores to match the row key normalisation below.
|
|
6062
|
+
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
6063
|
+
dataFieldName = metric.fullPath.replace(/\./g, '_');
|
|
6064
|
+
} else if (metric.relations && metric.relations.length > 0) {
|
|
6065
|
+
// 2-part path from a nested provider: API drops the base provider alias.
|
|
6066
|
+
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
6067
|
+
const parts = metric.fullPath.split('.');
|
|
6068
|
+
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
6069
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
6070
|
+
|
|
6071
|
+
const pathParts = pathWithoutField.split('_');
|
|
6072
|
+
pathParts.shift(); // remove base provider alias
|
|
6073
|
+
const pathWithoutBase = pathParts.join('_');
|
|
6074
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6075
|
+
} else {
|
|
6076
|
+
// 2-part path from the base provider: API returns just the metric name.
|
|
6077
|
+
// Example: "mc.record_count" -> "record_count"
|
|
6078
|
+
dataFieldName = metric.metricName;
|
|
6079
|
+
}
|
|
6080
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.id] || metric.metricTitle || dataFieldName, parameters);
|
|
6081
|
+
return {
|
|
6082
|
+
// `field` is the instance id (unique even for duplicate columns
|
|
6083
|
+
// pointing at the same underlying metric); the actual value is
|
|
6084
|
+
// read from the row via valueGetter using dataFieldName, which is
|
|
6085
|
+
// the key the API response actually uses (shared across duplicates).
|
|
6086
|
+
field: metric.id.replace(/[.#]/g, '_'),
|
|
6087
|
+
headerName: headerName,
|
|
6088
|
+
flex: 1,
|
|
6089
|
+
minWidth: 150,
|
|
6090
|
+
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
6091
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
6092
|
+
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6093
|
+
sx: {
|
|
6094
|
+
display: 'flex',
|
|
6095
|
+
alignItems: 'center',
|
|
6096
|
+
gap: 0.5
|
|
6097
|
+
}
|
|
6098
|
+
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
6099
|
+
source: metricDef?.source
|
|
6100
|
+
}), /*#__PURE__*/React__namespace.default.createElement("span", null, headerName)),
|
|
6101
|
+
valueFormatter: value => formatValue(value, metricDef)
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
6104
|
+
|
|
6105
|
+
// Dimension
|
|
6106
|
+
const dim = item.data;
|
|
6107
|
+
let dataFieldName;
|
|
6108
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
6109
|
+
// From nested provider: drop the base provider alias
|
|
6110
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
6111
|
+
const parts = dim.fullPath.split('.');
|
|
6112
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
6113
|
+
const field = parts[1]; // e.g., "currency"
|
|
6114
|
+
|
|
6115
|
+
const pathParts = pathWithoutField.split('_');
|
|
6116
|
+
pathParts.shift(); // Remove base provider alias
|
|
6117
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
6118
|
+
|
|
6119
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6120
|
+
} else {
|
|
6121
|
+
// From base provider: keep the full path with underscore
|
|
6122
|
+
// Example: ba.created_at -> ba_created_at
|
|
6123
|
+
dataFieldName = dim.fullPath.replace('.', '_');
|
|
6124
|
+
}
|
|
6125
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.id] || dim.dimensionTitle || dataFieldName, parameters);
|
|
6126
|
+
const dimDef = dim.dimension;
|
|
6127
|
+
return {
|
|
6128
|
+
// Same rationale as the metric branch above: `field` must be unique
|
|
6129
|
+
// per instance, while the value always comes from the one real
|
|
6130
|
+
// underlying row key (dataFieldName), shared by every duplicate.
|
|
6131
|
+
field: dim.id.replace(/[.#]/g, '_'),
|
|
6132
|
+
headerName: headerName,
|
|
6133
|
+
flex: 1,
|
|
6134
|
+
minWidth: 150,
|
|
6135
|
+
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
6136
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
6137
|
+
valueFormatter: value => formatValue(value, dimDef)
|
|
6138
|
+
};
|
|
6139
|
+
};
|
|
5770
6140
|
const ReportDataGrid = ({
|
|
5771
6141
|
reportData,
|
|
5772
|
-
|
|
5773
|
-
metrics,
|
|
6142
|
+
columnItems = [],
|
|
5774
6143
|
loading,
|
|
5775
6144
|
onPageChange,
|
|
5776
6145
|
totalRows = 0,
|
|
@@ -5786,127 +6155,10 @@ const ReportDataGrid = ({
|
|
|
5786
6155
|
pageSize: 50
|
|
5787
6156
|
});
|
|
5788
6157
|
|
|
5789
|
-
// Generate columns dynamically
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
// Add dimension + computed-dimension columns, in the single unified
|
|
5794
|
-
// order the user arranged them in on the Dimensions tab.
|
|
5795
|
-
// Field naming logic (dimensions):
|
|
5796
|
-
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5797
|
-
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5798
|
-
// Computed dimensions have no provider/relations chain, so the API
|
|
5799
|
-
// returns each row keyed by the computed dimension's own `name`.
|
|
5800
|
-
orderedDimensionItems.forEach(item => {
|
|
5801
|
-
if (item.kind === 'computed') {
|
|
5802
|
-
const cd = item.data;
|
|
5803
|
-
cols.push({
|
|
5804
|
-
field: cd.name,
|
|
5805
|
-
headerName: cd.name,
|
|
5806
|
-
flex: 1,
|
|
5807
|
-
minWidth: 150,
|
|
5808
|
-
type: 'string'
|
|
5809
|
-
});
|
|
5810
|
-
return;
|
|
5811
|
-
}
|
|
5812
|
-
const dim = item.data;
|
|
5813
|
-
let fieldName;
|
|
5814
|
-
let headerName;
|
|
5815
|
-
|
|
5816
|
-
// Check if there are relations (nested providers)
|
|
5817
|
-
if (dim.relations && dim.relations.length > 0) {
|
|
5818
|
-
// From nested provider: drop the base provider alias
|
|
5819
|
-
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5820
|
-
const parts = dim.fullPath.split('.');
|
|
5821
|
-
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5822
|
-
const field = parts[1]; // e.g., "currency"
|
|
5823
|
-
|
|
5824
|
-
// Remove the base provider alias (first part before first underscore)
|
|
5825
|
-
const pathParts = pathWithoutField.split('_');
|
|
5826
|
-
pathParts.shift(); // Remove base provider alias
|
|
5827
|
-
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5828
|
-
|
|
5829
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5830
|
-
} else {
|
|
5831
|
-
// From base provider: keep the full path with underscore
|
|
5832
|
-
// Example: ba.created_at -> ba_created_at
|
|
5833
|
-
fieldName = dim.fullPath.replace('.', '_');
|
|
5834
|
-
}
|
|
5835
|
-
|
|
5836
|
-
// Check for title override, otherwise use the friendly dimension title from provider
|
|
5837
|
-
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
5838
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5839
|
-
const dimDef = dim.dimension;
|
|
5840
|
-
cols.push({
|
|
5841
|
-
field: fieldName,
|
|
5842
|
-
headerName: headerName,
|
|
5843
|
-
flex: 1,
|
|
5844
|
-
minWidth: 150,
|
|
5845
|
-
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5846
|
-
valueFormatter: value => formatValue(value, dimDef)
|
|
5847
|
-
});
|
|
5848
|
-
});
|
|
5849
|
-
|
|
5850
|
-
// Add metric columns
|
|
5851
|
-
// The API returns metric keys in two different forms depending on the path depth:
|
|
5852
|
-
//
|
|
5853
|
-
// • 2-part path "mc.record_count" → API key: "record_count"
|
|
5854
|
-
// • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
|
|
5855
|
-
// • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
|
|
5856
|
-
//
|
|
5857
|
-
// For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
|
|
5858
|
-
// in field names as nested-object accessors, so the rows useMemo normalises
|
|
5859
|
-
// those keys (dots → underscores). The column field must match that normalised key.
|
|
5860
|
-
metrics.forEach(metric => {
|
|
5861
|
-
const metricDef = metric.metric;
|
|
5862
|
-
let fieldName;
|
|
5863
|
-
let headerName;
|
|
5864
|
-
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5865
|
-
if (dotCount >= 2) {
|
|
5866
|
-
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5867
|
-
// Normalise dots to underscores to match the row key normalisation below.
|
|
5868
|
-
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5869
|
-
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5870
|
-
} else if (metric.relations && metric.relations.length > 0) {
|
|
5871
|
-
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5872
|
-
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5873
|
-
const parts = metric.fullPath.split('.');
|
|
5874
|
-
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5875
|
-
const field = parts[1]; // e.g., "total_amount"
|
|
5876
|
-
|
|
5877
|
-
const pathParts = pathWithoutField.split('_');
|
|
5878
|
-
pathParts.shift(); // remove base provider alias
|
|
5879
|
-
const pathWithoutBase = pathParts.join('_');
|
|
5880
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5881
|
-
} else {
|
|
5882
|
-
// 2-part path from the base provider: API returns just the metric name.
|
|
5883
|
-
// Example: "mc.record_count" -> "record_count"
|
|
5884
|
-
fieldName = metric.metricName;
|
|
5885
|
-
}
|
|
5886
|
-
|
|
5887
|
-
// Check for title override, otherwise use the friendly metric title from provider
|
|
5888
|
-
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
5889
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5890
|
-
cols.push({
|
|
5891
|
-
field: fieldName,
|
|
5892
|
-
headerName: headerName,
|
|
5893
|
-
flex: 1,
|
|
5894
|
-
minWidth: 150,
|
|
5895
|
-
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5896
|
-
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5897
|
-
sx: {
|
|
5898
|
-
display: 'flex',
|
|
5899
|
-
alignItems: 'center',
|
|
5900
|
-
gap: 0.5
|
|
5901
|
-
}
|
|
5902
|
-
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
5903
|
-
source: metricDef?.source
|
|
5904
|
-
}), /*#__PURE__*/React__namespace.default.createElement("span", null, headerName)),
|
|
5905
|
-
valueFormatter: value => formatValue(value, metricDef)
|
|
5906
|
-
});
|
|
5907
|
-
});
|
|
5908
|
-
return cols;
|
|
5909
|
-
}, [orderedDimensionItems, metrics, titleOverrides, parameters]);
|
|
6158
|
+
// Generate columns dynamically, in the final resolved column order
|
|
6159
|
+
// (custom Column Order tab arrangement when defined, otherwise
|
|
6160
|
+
// dimensions/computed then metrics).
|
|
6161
|
+
const columns = React__namespace.default.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
|
|
5910
6162
|
|
|
5911
6163
|
// Transform report data to rows with unique IDs.
|
|
5912
6164
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -6016,7 +6268,12 @@ const ReportBuilder = ({
|
|
|
6016
6268
|
computedDimensions: [],
|
|
6017
6269
|
// Unified display/execution order across dimensions + computedDimensions.
|
|
6018
6270
|
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
6019
|
-
dimensionOrder: []
|
|
6271
|
+
dimensionOrder: [],
|
|
6272
|
+
// Optional unified display order across dimensions + computedDimensions +
|
|
6273
|
+
// metrics, set on the Column Order tab. Empty means "not defined" — the
|
|
6274
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6275
|
+
// Entries: { type: 'dimension' | 'computed' | 'metric', key }
|
|
6276
|
+
columnOrder: []
|
|
6020
6277
|
});
|
|
6021
6278
|
const [titleOverrides, setTitleOverrides] = React.useState({
|
|
6022
6279
|
dimensions: {},
|
|
@@ -6202,6 +6459,72 @@ const ReportBuilder = ({
|
|
|
6202
6459
|
}
|
|
6203
6460
|
};
|
|
6204
6461
|
|
|
6462
|
+
// Reconstructs saved dimensions as independent, individually-identified
|
|
6463
|
+
// instances so that deliberate duplicates (the same field added more than
|
|
6464
|
+
// once) survive a save/reload round trip. Prefers `ordered_dimensions`
|
|
6465
|
+
// (one ref per instance - the nth duplicate of a path is suffixed
|
|
6466
|
+
// `#n`) when present; falls back to one instance per entry in the
|
|
6467
|
+
// (necessarily duplicate-free) `dimensions` path list for reports saved
|
|
6468
|
+
// before this field existed.
|
|
6469
|
+
const reconstructDimensions = (reportDef, providersData) => {
|
|
6470
|
+
const orderByMap = {};
|
|
6471
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6472
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6473
|
+
orderByMap[orderItem.name] = orderItem.desc === true ? "desc" : "asc";
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
const reconstructed = [];
|
|
6477
|
+
const orderedRefs = (reportDef.definition?.doc?.query?.ordered_dimensions || []).filter(entry => entry.type === "dimension").map(entry => entry.ref);
|
|
6478
|
+
if (orderedRefs.length > 0) {
|
|
6479
|
+
orderedRefs.forEach(ref => {
|
|
6480
|
+
const fullPath = ref.split("#")[0];
|
|
6481
|
+
const dim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
6482
|
+
if (dim) {
|
|
6483
|
+
dim.id = ref;
|
|
6484
|
+
dim.sortOrder = orderByMap[fullPath] || null;
|
|
6485
|
+
reconstructed.push(dim);
|
|
6486
|
+
}
|
|
6487
|
+
});
|
|
6488
|
+
} else if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6489
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6490
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6491
|
+
if (dim) {
|
|
6492
|
+
dim.id = dimPath;
|
|
6493
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6494
|
+
reconstructed.push(dim);
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
return reconstructed;
|
|
6499
|
+
};
|
|
6500
|
+
|
|
6501
|
+
// Mirrors reconstructDimensions for metrics, using the analogous
|
|
6502
|
+
// `ordered_metrics` field (metrics have no other unified-order concept to
|
|
6503
|
+
// fall back on, so this is always written whenever there are metrics).
|
|
6504
|
+
const reconstructMetrics = (reportDef, providersData) => {
|
|
6505
|
+
const reconstructed = [];
|
|
6506
|
+
const orderedRefs = reportDef.definition?.doc?.query?.ordered_metrics || [];
|
|
6507
|
+
if (orderedRefs.length > 0) {
|
|
6508
|
+
orderedRefs.forEach(ref => {
|
|
6509
|
+
const fullPath = ref.split("#")[0];
|
|
6510
|
+
const metric = reconstructMetricFromPath(fullPath, providersData, reportDef.provider);
|
|
6511
|
+
if (metric) {
|
|
6512
|
+
metric.id = ref;
|
|
6513
|
+
reconstructed.push(metric);
|
|
6514
|
+
}
|
|
6515
|
+
});
|
|
6516
|
+
} else if (reportDef.definition?.doc?.query?.metrics) {
|
|
6517
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6518
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6519
|
+
if (metric) {
|
|
6520
|
+
metric.id = metricPath;
|
|
6521
|
+
reconstructed.push(metric);
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
return reconstructed;
|
|
6526
|
+
};
|
|
6527
|
+
|
|
6205
6528
|
// Build the unified display/execution order for dimensions + computed
|
|
6206
6529
|
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6207
6530
|
// to "dimensions first, then computed dimensions" for report definitions
|
|
@@ -6212,7 +6535,7 @@ const ReportBuilder = ({
|
|
|
6212
6535
|
const seenComputedKeys = new Set();
|
|
6213
6536
|
if (Array.isArray(rawOrderedDimensions)) {
|
|
6214
6537
|
rawOrderedDimensions.forEach(entry => {
|
|
6215
|
-
if (entry.type === "dimension" && dimensions.some(d => d.
|
|
6538
|
+
if (entry.type === "dimension" && dimensions.some(d => d.id === entry.ref)) {
|
|
6216
6539
|
dimensionOrder.push({
|
|
6217
6540
|
type: "dimension",
|
|
6218
6541
|
key: entry.ref
|
|
@@ -6230,10 +6553,10 @@ const ReportBuilder = ({
|
|
|
6230
6553
|
|
|
6231
6554
|
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6232
6555
|
dimensions.forEach(d => {
|
|
6233
|
-
if (!seenDimensionKeys.has(d.
|
|
6556
|
+
if (!seenDimensionKeys.has(d.id)) {
|
|
6234
6557
|
dimensionOrder.push({
|
|
6235
6558
|
type: "dimension",
|
|
6236
|
-
key: d.
|
|
6559
|
+
key: d.id
|
|
6237
6560
|
});
|
|
6238
6561
|
}
|
|
6239
6562
|
});
|
|
@@ -6247,6 +6570,46 @@ const ReportBuilder = ({
|
|
|
6247
6570
|
});
|
|
6248
6571
|
return dimensionOrder;
|
|
6249
6572
|
};
|
|
6573
|
+
|
|
6574
|
+
// Build the optional unified column order (dimensions + computed
|
|
6575
|
+
// dimensions + metrics) from the persisted `ordered_columns` field.
|
|
6576
|
+
// Unlike buildDimensionOrder, entries that no longer resolve are simply
|
|
6577
|
+
// dropped rather than backfilled — an empty/partial result just means "no
|
|
6578
|
+
// custom order defined (yet)", which resolveColumnOrder treats as
|
|
6579
|
+
// "fall back to the default order".
|
|
6580
|
+
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6581
|
+
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6582
|
+
const dimensionIds = new Set(dimensions.map(d => d.id));
|
|
6583
|
+
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6584
|
+
const metricIds = new Set(metrics.map(m => m.id));
|
|
6585
|
+
const columnOrder = [];
|
|
6586
|
+
rawOrderedColumns.forEach(entry => {
|
|
6587
|
+
if (entry.type === "dimension" && dimensionIds.has(entry.ref)) {
|
|
6588
|
+
columnOrder.push({
|
|
6589
|
+
type: "dimension",
|
|
6590
|
+
key: entry.ref
|
|
6591
|
+
});
|
|
6592
|
+
} else if (entry.type === "computed" && computedNames.has(entry.ref)) {
|
|
6593
|
+
columnOrder.push({
|
|
6594
|
+
type: "computed",
|
|
6595
|
+
key: entry.ref
|
|
6596
|
+
});
|
|
6597
|
+
} else if (entry.type === "metric" && metricIds.has(entry.ref)) {
|
|
6598
|
+
columnOrder.push({
|
|
6599
|
+
type: "metric",
|
|
6600
|
+
key: entry.ref
|
|
6601
|
+
});
|
|
6602
|
+
}
|
|
6603
|
+
});
|
|
6604
|
+
return columnOrder;
|
|
6605
|
+
};
|
|
6606
|
+
|
|
6607
|
+
// Auto-append a newly added dimension/computed dimension/metric to the end
|
|
6608
|
+
// of columnOrder — but only once a custom order actually exists. While
|
|
6609
|
+
// columnOrder is empty (no custom order defined) it's left untouched, so
|
|
6610
|
+
// the results grid keeps falling back to the default dimensions-then-
|
|
6611
|
+
// metrics order until the user visits the Column Order tab.
|
|
6612
|
+
const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
|
|
6250
6613
|
const loadReportDefinition = async id => {
|
|
6251
6614
|
try {
|
|
6252
6615
|
console.log("Loading report definition:", id);
|
|
@@ -6261,43 +6624,10 @@ const ReportBuilder = ({
|
|
|
6261
6624
|
// Set the title
|
|
6262
6625
|
setReportTitle(reportDef.title || "");
|
|
6263
6626
|
|
|
6264
|
-
// Reconstruct dimensions
|
|
6265
|
-
|
|
6266
|
-
const
|
|
6267
|
-
|
|
6268
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
6269
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6270
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6271
|
-
if (orderItem.desc === true) {
|
|
6272
|
-
orderByMap[orderItem.name] = "desc";
|
|
6273
|
-
} else {
|
|
6274
|
-
orderByMap[orderItem.name] = "asc";
|
|
6275
|
-
}
|
|
6276
|
-
}
|
|
6277
|
-
}
|
|
6278
|
-
|
|
6279
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
6280
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6281
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6282
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6283
|
-
if (dim) {
|
|
6284
|
-
// Add sortOrder from order_by if it exists
|
|
6285
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6286
|
-
reconstructedDimensions.push(dim);
|
|
6287
|
-
}
|
|
6288
|
-
}
|
|
6289
|
-
}
|
|
6290
|
-
|
|
6291
|
-
// Reconstruct metrics
|
|
6292
|
-
const reconstructedMetrics = [];
|
|
6293
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
6294
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6295
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6296
|
-
if (metric) {
|
|
6297
|
-
reconstructedMetrics.push(metric);
|
|
6298
|
-
}
|
|
6299
|
-
}
|
|
6300
|
-
}
|
|
6627
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
6628
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
6629
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
6630
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
6301
6631
|
|
|
6302
6632
|
// Load title overrides if they exist
|
|
6303
6633
|
const loadedTitleOverrides = {
|
|
@@ -6343,6 +6673,7 @@ const ReportBuilder = ({
|
|
|
6343
6673
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6344
6674
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6345
6675
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6676
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6346
6677
|
|
|
6347
6678
|
// Set the report state
|
|
6348
6679
|
setReport({
|
|
@@ -6350,7 +6681,8 @@ const ReportBuilder = ({
|
|
|
6350
6681
|
metrics: reconstructedMetrics,
|
|
6351
6682
|
filters: loadedFilters,
|
|
6352
6683
|
computedDimensions,
|
|
6353
|
-
dimensionOrder
|
|
6684
|
+
dimensionOrder,
|
|
6685
|
+
columnOrder
|
|
6354
6686
|
});
|
|
6355
6687
|
|
|
6356
6688
|
// Set title overrides
|
|
@@ -6379,43 +6711,10 @@ const ReportBuilder = ({
|
|
|
6379
6711
|
// Set the title (already modified with " (Copy)" suffix)
|
|
6380
6712
|
setReportTitle(reportDef.title || "");
|
|
6381
6713
|
|
|
6382
|
-
// Reconstruct dimensions
|
|
6383
|
-
|
|
6384
|
-
const
|
|
6385
|
-
|
|
6386
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
6387
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6388
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6389
|
-
if (orderItem.desc === true) {
|
|
6390
|
-
orderByMap[orderItem.name] = "desc";
|
|
6391
|
-
} else {
|
|
6392
|
-
orderByMap[orderItem.name] = "asc";
|
|
6393
|
-
}
|
|
6394
|
-
}
|
|
6395
|
-
}
|
|
6396
|
-
|
|
6397
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
6398
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6399
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6400
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6401
|
-
if (dim) {
|
|
6402
|
-
// Add sortOrder from order_by if it exists
|
|
6403
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6404
|
-
reconstructedDimensions.push(dim);
|
|
6405
|
-
}
|
|
6406
|
-
}
|
|
6407
|
-
}
|
|
6408
|
-
|
|
6409
|
-
// Reconstruct metrics
|
|
6410
|
-
const reconstructedMetrics = [];
|
|
6411
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
6412
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6413
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6414
|
-
if (metric) {
|
|
6415
|
-
reconstructedMetrics.push(metric);
|
|
6416
|
-
}
|
|
6417
|
-
}
|
|
6418
|
-
}
|
|
6714
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
6715
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
6716
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
6717
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
6419
6718
|
|
|
6420
6719
|
// Load title overrides if they exist
|
|
6421
6720
|
const loadedTitleOverrides = {
|
|
@@ -6460,6 +6759,7 @@ const ReportBuilder = ({
|
|
|
6460
6759
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6461
6760
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6462
6761
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6762
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6463
6763
|
|
|
6464
6764
|
// Set the report state
|
|
6465
6765
|
setReport({
|
|
@@ -6467,7 +6767,8 @@ const ReportBuilder = ({
|
|
|
6467
6767
|
metrics: reconstructedMetrics,
|
|
6468
6768
|
filters: loadedFilters,
|
|
6469
6769
|
computedDimensions,
|
|
6470
|
-
dimensionOrder
|
|
6770
|
+
dimensionOrder,
|
|
6771
|
+
columnOrder
|
|
6471
6772
|
});
|
|
6472
6773
|
|
|
6473
6774
|
// Set title overrides
|
|
@@ -6501,7 +6802,8 @@ const ReportBuilder = ({
|
|
|
6501
6802
|
metrics: [],
|
|
6502
6803
|
filters: {},
|
|
6503
6804
|
computedDimensions: [],
|
|
6504
|
-
dimensionOrder: []
|
|
6805
|
+
dimensionOrder: [],
|
|
6806
|
+
columnOrder: []
|
|
6505
6807
|
});
|
|
6506
6808
|
// Reset title overrides
|
|
6507
6809
|
setTitleOverrides({
|
|
@@ -6511,42 +6813,45 @@ const ReportBuilder = ({
|
|
|
6511
6813
|
});
|
|
6512
6814
|
console.log("Selected root provider:", event.target.value);
|
|
6513
6815
|
};
|
|
6514
|
-
|
|
6816
|
+
|
|
6817
|
+
// Keyed by instance id, so two instances of the same dimension can carry
|
|
6818
|
+
// independent custom titles.
|
|
6819
|
+
const handleUpdateDimensionTitle = (id, customTitle) => {
|
|
6515
6820
|
setTitleOverrides(prev => ({
|
|
6516
6821
|
...prev,
|
|
6517
6822
|
dimensions: {
|
|
6518
6823
|
...prev.dimensions,
|
|
6519
|
-
[
|
|
6824
|
+
[id]: customTitle
|
|
6520
6825
|
}
|
|
6521
6826
|
}));
|
|
6522
6827
|
};
|
|
6523
|
-
const handleResetDimensionTitle =
|
|
6828
|
+
const handleResetDimensionTitle = id => {
|
|
6524
6829
|
setTitleOverrides(prev => {
|
|
6525
6830
|
const newDimensions = {
|
|
6526
6831
|
...prev.dimensions
|
|
6527
6832
|
};
|
|
6528
|
-
delete newDimensions[
|
|
6833
|
+
delete newDimensions[id];
|
|
6529
6834
|
return {
|
|
6530
6835
|
...prev,
|
|
6531
6836
|
dimensions: newDimensions
|
|
6532
6837
|
};
|
|
6533
6838
|
});
|
|
6534
6839
|
};
|
|
6535
|
-
const handleUpdateMetricTitle = (
|
|
6840
|
+
const handleUpdateMetricTitle = (id, customTitle) => {
|
|
6536
6841
|
setTitleOverrides(prev => ({
|
|
6537
6842
|
...prev,
|
|
6538
6843
|
metrics: {
|
|
6539
6844
|
...prev.metrics,
|
|
6540
|
-
[
|
|
6845
|
+
[id]: customTitle
|
|
6541
6846
|
}
|
|
6542
6847
|
}));
|
|
6543
6848
|
};
|
|
6544
|
-
const handleResetMetricTitle =
|
|
6849
|
+
const handleResetMetricTitle = id => {
|
|
6545
6850
|
setTitleOverrides(prev => {
|
|
6546
6851
|
const newMetrics = {
|
|
6547
6852
|
...prev.metrics
|
|
6548
6853
|
};
|
|
6549
|
-
delete newMetrics[
|
|
6854
|
+
delete newMetrics[id];
|
|
6550
6855
|
return {
|
|
6551
6856
|
...prev,
|
|
6552
6857
|
metrics: newMetrics
|
|
@@ -6581,29 +6886,42 @@ const ReportBuilder = ({
|
|
|
6581
6886
|
dimensions: [...prev.dimensions, dimensionData],
|
|
6582
6887
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6583
6888
|
type: "dimension",
|
|
6584
|
-
key: dimensionData.
|
|
6585
|
-
}]
|
|
6889
|
+
key: dimensionData.id
|
|
6890
|
+
}],
|
|
6891
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6892
|
+
type: "dimension",
|
|
6893
|
+
key: dimensionData.id
|
|
6894
|
+
})
|
|
6586
6895
|
};
|
|
6587
6896
|
console.log("Dimension saved:", dimensionData);
|
|
6588
6897
|
console.log("Complete report:", newReport);
|
|
6589
6898
|
return newReport;
|
|
6590
6899
|
});
|
|
6591
6900
|
};
|
|
6592
|
-
const handleRemoveDimension =
|
|
6901
|
+
const handleRemoveDimension = id => {
|
|
6593
6902
|
setReport(prev => ({
|
|
6594
6903
|
...prev,
|
|
6595
|
-
dimensions: prev.dimensions.filter(d => d.
|
|
6596
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key ===
|
|
6904
|
+
dimensions: prev.dimensions.filter(d => d.id !== id),
|
|
6905
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === id)),
|
|
6906
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === id))
|
|
6597
6907
|
}));
|
|
6598
6908
|
};
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6909
|
+
|
|
6910
|
+
// sortOrder is fundamentally a per-field (query-level) setting - SQL can
|
|
6911
|
+
// only sort by a given column once - so toggling it on any instance of a
|
|
6912
|
+
// duplicated dimension applies to every instance sharing that fullPath.
|
|
6913
|
+
const handleUpdateDimensionSortOrder = (id, sortOrder) => {
|
|
6914
|
+
setReport(prev => {
|
|
6915
|
+
const target = prev.dimensions.find(d => d.id === id);
|
|
6916
|
+
if (!target) return prev;
|
|
6917
|
+
return {
|
|
6918
|
+
...prev,
|
|
6919
|
+
dimensions: prev.dimensions.map(d => d.fullPath === target.fullPath ? {
|
|
6920
|
+
...d,
|
|
6921
|
+
sortOrder
|
|
6922
|
+
} : d)
|
|
6923
|
+
};
|
|
6924
|
+
});
|
|
6607
6925
|
};
|
|
6608
6926
|
|
|
6609
6927
|
// Reorders the single unified dimensions + computed-dimensions list;
|
|
@@ -6622,7 +6940,11 @@ const ReportBuilder = ({
|
|
|
6622
6940
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6623
6941
|
type: "computed",
|
|
6624
6942
|
key: computedDimensionData.name
|
|
6625
|
-
}]
|
|
6943
|
+
}],
|
|
6944
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6945
|
+
type: "computed",
|
|
6946
|
+
key: computedDimensionData.name
|
|
6947
|
+
})
|
|
6626
6948
|
}));
|
|
6627
6949
|
};
|
|
6628
6950
|
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
@@ -6632,6 +6954,10 @@ const ReportBuilder = ({
|
|
|
6632
6954
|
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6633
6955
|
...entry,
|
|
6634
6956
|
key: computedDimensionData.name
|
|
6957
|
+
} : entry),
|
|
6958
|
+
columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6959
|
+
...entry,
|
|
6960
|
+
key: computedDimensionData.name
|
|
6635
6961
|
} : entry)
|
|
6636
6962
|
}));
|
|
6637
6963
|
};
|
|
@@ -6639,24 +6965,30 @@ const ReportBuilder = ({
|
|
|
6639
6965
|
setReport(prev => ({
|
|
6640
6966
|
...prev,
|
|
6641
6967
|
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6642
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6968
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
|
|
6969
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6643
6970
|
}));
|
|
6644
6971
|
};
|
|
6645
6972
|
const handleSaveMetric = metricData => {
|
|
6646
6973
|
setReport(prev => {
|
|
6647
6974
|
const newReport = {
|
|
6648
6975
|
...prev,
|
|
6649
|
-
metrics: [...prev.metrics, metricData]
|
|
6976
|
+
metrics: [...prev.metrics, metricData],
|
|
6977
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6978
|
+
type: "metric",
|
|
6979
|
+
key: metricData.id
|
|
6980
|
+
})
|
|
6650
6981
|
};
|
|
6651
6982
|
console.log("Metric saved:", metricData);
|
|
6652
6983
|
console.log("Complete report:", newReport);
|
|
6653
6984
|
return newReport;
|
|
6654
6985
|
});
|
|
6655
6986
|
};
|
|
6656
|
-
const handleRemoveMetric =
|
|
6987
|
+
const handleRemoveMetric = id => {
|
|
6657
6988
|
setReport(prev => ({
|
|
6658
6989
|
...prev,
|
|
6659
|
-
metrics: prev.metrics.filter(
|
|
6990
|
+
metrics: prev.metrics.filter(m => m.id !== id),
|
|
6991
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === id))
|
|
6660
6992
|
}));
|
|
6661
6993
|
};
|
|
6662
6994
|
const handleReorderMetrics = newOrder => {
|
|
@@ -6665,6 +6997,16 @@ const ReportBuilder = ({
|
|
|
6665
6997
|
metrics: newOrder
|
|
6666
6998
|
}));
|
|
6667
6999
|
};
|
|
7000
|
+
|
|
7001
|
+
// Sets the unified column order shown on the Column Order tab. An empty
|
|
7002
|
+
// array (e.g. via "Reset to Default") clears the customization, and the
|
|
7003
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
7004
|
+
const handleReorderColumnItems = newColumnOrder => {
|
|
7005
|
+
setReport(prev => ({
|
|
7006
|
+
...prev,
|
|
7007
|
+
columnOrder: newColumnOrder
|
|
7008
|
+
}));
|
|
7009
|
+
};
|
|
6668
7010
|
const handleSaveFilter = (fullPath, filterData) => {
|
|
6669
7011
|
setReport(prev => {
|
|
6670
7012
|
const newReport = {
|
|
@@ -6702,13 +7044,30 @@ const ReportBuilder = ({
|
|
|
6702
7044
|
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6703
7045
|
// order the user arranged on screen.
|
|
6704
7046
|
const buildQueryObject = () => {
|
|
6705
|
-
const
|
|
7047
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
6706
7048
|
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6707
|
-
|
|
7049
|
+
|
|
7050
|
+
// May contain more than one instance of the same fullPath (deliberate
|
|
7051
|
+
// duplicates), kept in dimensionOrder's display order.
|
|
7052
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionById[entry.key]).filter(Boolean);
|
|
6708
7053
|
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6709
7054
|
|
|
7055
|
+
// The API keys each result row by fullPath, so duplicate instances of
|
|
7056
|
+
// the same field can only ever carry one value/one sort direction at
|
|
7057
|
+
// the query level - dedupe by fullPath (keeping first-occurrence order)
|
|
7058
|
+
// before sending. The builder still shows/orders/titles each instance
|
|
7059
|
+
// independently; they just read from the same underlying query column.
|
|
7060
|
+
const uniqueDimensionsByPath = [];
|
|
7061
|
+
const seenDimensionPaths = new Set();
|
|
7062
|
+
orderedDimensionsOnly.forEach(dim => {
|
|
7063
|
+
if (!seenDimensionPaths.has(dim.fullPath)) {
|
|
7064
|
+
seenDimensionPaths.add(dim.fullPath);
|
|
7065
|
+
uniqueDimensionsByPath.push(dim);
|
|
7066
|
+
}
|
|
7067
|
+
});
|
|
7068
|
+
|
|
6710
7069
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6711
|
-
const orderBy =
|
|
7070
|
+
const orderBy = uniqueDimensionsByPath.map(dim => {
|
|
6712
7071
|
const orderItem = {
|
|
6713
7072
|
name: dim.fullPath
|
|
6714
7073
|
};
|
|
@@ -6763,9 +7122,20 @@ const ReportBuilder = ({
|
|
|
6763
7122
|
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
6764
7123
|
titles.filters = titleOverrides.filters;
|
|
6765
7124
|
}
|
|
7125
|
+
|
|
7126
|
+
// Same dedupe rationale as dimensions above: the query only needs each
|
|
7127
|
+
// metric fullPath once, however many display instances reference it.
|
|
7128
|
+
const uniqueMetricPaths = [];
|
|
7129
|
+
const seenMetricPaths = new Set();
|
|
7130
|
+
report.metrics.forEach(metric => {
|
|
7131
|
+
if (!seenMetricPaths.has(metric.fullPath)) {
|
|
7132
|
+
seenMetricPaths.add(metric.fullPath);
|
|
7133
|
+
uniqueMetricPaths.push(metric.fullPath);
|
|
7134
|
+
}
|
|
7135
|
+
});
|
|
6766
7136
|
const queryObj = {
|
|
6767
|
-
dimensions:
|
|
6768
|
-
metrics:
|
|
7137
|
+
dimensions: uniqueDimensionsByPath.map(dim => dim.fullPath),
|
|
7138
|
+
metrics: uniqueMetricPaths,
|
|
6769
7139
|
order_by: orderBy
|
|
6770
7140
|
};
|
|
6771
7141
|
|
|
@@ -6779,7 +7149,9 @@ const ReportBuilder = ({
|
|
|
6779
7149
|
|
|
6780
7150
|
// Persist the unified dimension + computed-dimension order so it can be
|
|
6781
7151
|
// reconstructed on reload and so executed reports render columns in the
|
|
6782
|
-
// same order they were arranged in the builder.
|
|
7152
|
+
// same order they were arranged in the builder. Entries use each
|
|
7153
|
+
// dimension's instance id as `ref`, so deliberate duplicates (the nth
|
|
7154
|
+
// occurrence of a fullPath is suffixed `#n`) round-trip correctly.
|
|
6783
7155
|
if (report.dimensionOrder.length > 0) {
|
|
6784
7156
|
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
6785
7157
|
type: entry.type,
|
|
@@ -6787,6 +7159,23 @@ const ReportBuilder = ({
|
|
|
6787
7159
|
}));
|
|
6788
7160
|
}
|
|
6789
7161
|
|
|
7162
|
+
// Persist metric instance order/identity the same way (metrics have no
|
|
7163
|
+
// other unified-order field to piggyback on).
|
|
7164
|
+
if (report.metrics.length > 0) {
|
|
7165
|
+
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
7166
|
+
}
|
|
7167
|
+
|
|
7168
|
+
// Persist the optional unified column order (Column Order tab) so the
|
|
7169
|
+
// results grid can reproduce it on reload. Absent when no custom order
|
|
7170
|
+
// has been defined, so old/untouched reports keep their current
|
|
7171
|
+
// dimensions-then-metrics column order unchanged.
|
|
7172
|
+
if (report.columnOrder.length > 0) {
|
|
7173
|
+
queryObj.ordered_columns = report.columnOrder.map(entry => ({
|
|
7174
|
+
type: entry.type,
|
|
7175
|
+
ref: entry.key
|
|
7176
|
+
}));
|
|
7177
|
+
}
|
|
7178
|
+
|
|
6790
7179
|
// Only add titles if there are any overrides
|
|
6791
7180
|
if (Object.keys(titles).length > 0) {
|
|
6792
7181
|
queryObj.titles = titles;
|
|
@@ -6846,8 +7235,8 @@ const ReportBuilder = ({
|
|
|
6846
7235
|
const handleRunReport = async () => {
|
|
6847
7236
|
setCurrentPage(0);
|
|
6848
7237
|
await runReportWithPagination(0, pageSize);
|
|
6849
|
-
// Switch to Results tab after running report (
|
|
6850
|
-
setActiveTab(
|
|
7238
|
+
// Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
|
|
7239
|
+
setActiveTab(4);
|
|
6851
7240
|
};
|
|
6852
7241
|
const handleDownloadReport = async () => {
|
|
6853
7242
|
try {
|
|
@@ -6945,15 +7334,20 @@ const ReportBuilder = ({
|
|
|
6945
7334
|
|
|
6946
7335
|
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
6947
7336
|
// objects so the results grid can render columns in that same order.
|
|
6948
|
-
const
|
|
7337
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
6949
7338
|
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6950
7339
|
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
6951
7340
|
kind: "dimension",
|
|
6952
|
-
data:
|
|
7341
|
+
data: dimensionById[entry.key]
|
|
6953
7342
|
} : {
|
|
6954
7343
|
kind: "computed",
|
|
6955
7344
|
data: computedDimensionByName[entry.key]
|
|
6956
7345
|
}).filter(item => item.data);
|
|
7346
|
+
|
|
7347
|
+
// Final column list for the results grid: the explicit Column Order tab
|
|
7348
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7349
|
+
// order) followed by metrics (in their own order) — today's behavior.
|
|
7350
|
+
const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
6957
7351
|
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6958
7352
|
sx: {
|
|
6959
7353
|
p: 3,
|
|
@@ -7208,9 +7602,36 @@ const ReportBuilder = ({
|
|
|
7208
7602
|
}
|
|
7209
7603
|
}
|
|
7210
7604
|
}), /*#__PURE__*/React__namespace.default.createElement(material.Tab, {
|
|
7211
|
-
label:
|
|
7605
|
+
label: /*#__PURE__*/React__namespace.default.createElement(material.Badge, {
|
|
7606
|
+
variant: "dot",
|
|
7607
|
+
invisible: report.columnOrder.length === 0,
|
|
7608
|
+
sx: {
|
|
7609
|
+
"& .MuiBadge-dot": {
|
|
7610
|
+
backgroundColor: "rgb(70, 134, 128)"
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7613
|
+
}, /*#__PURE__*/React__namespace.default.createElement("span", {
|
|
7614
|
+
style: {
|
|
7615
|
+
marginRight: report.columnOrder.length > 0 ? "8px" : "0"
|
|
7616
|
+
}
|
|
7617
|
+
}, "Column Order")),
|
|
7212
7618
|
id: "report-tab-3",
|
|
7213
7619
|
"aria-controls": "report-tabpanel-3",
|
|
7620
|
+
sx: {
|
|
7621
|
+
height: "41px",
|
|
7622
|
+
fontFamily: "system-ui",
|
|
7623
|
+
borderRadius: "0.5rem",
|
|
7624
|
+
boxShadow: "none",
|
|
7625
|
+
textTransform: "none",
|
|
7626
|
+
color: "rgb(37, 37, 37)",
|
|
7627
|
+
"&.Mui-selected": {
|
|
7628
|
+
color: "rgb(70, 134, 128)"
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7631
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Tab, {
|
|
7632
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
7633
|
+
id: "report-tab-4",
|
|
7634
|
+
"aria-controls": "report-tabpanel-4",
|
|
7214
7635
|
disabled: !reportData,
|
|
7215
7636
|
sx: {
|
|
7216
7637
|
height: "41px",
|
|
@@ -7277,10 +7698,18 @@ const ReportBuilder = ({
|
|
|
7277
7698
|
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7278
7699
|
value: activeTab,
|
|
7279
7700
|
index: 3
|
|
7280
|
-
},
|
|
7281
|
-
reportData: reportData,
|
|
7701
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ColumnOrder, {
|
|
7282
7702
|
orderedDimensionItems: orderedDimensionItems,
|
|
7283
7703
|
metrics: report.metrics,
|
|
7704
|
+
columnOrder: report.columnOrder,
|
|
7705
|
+
onReorderColumnItems: handleReorderColumnItems,
|
|
7706
|
+
titleOverrides: titleOverrides
|
|
7707
|
+
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7708
|
+
value: activeTab,
|
|
7709
|
+
index: 4
|
|
7710
|
+
}, reportData && /*#__PURE__*/React__namespace.default.createElement(ReportDataGrid, {
|
|
7711
|
+
reportData: reportData,
|
|
7712
|
+
columnItems: columnItems,
|
|
7284
7713
|
loading: loading,
|
|
7285
7714
|
onPageChange: handlePageChange,
|
|
7286
7715
|
totalRows: totalRows,
|
|
@@ -7368,6 +7797,12 @@ const CurrencySelector = () => {
|
|
|
7368
7797
|
parameters,
|
|
7369
7798
|
setParameters
|
|
7370
7799
|
} = useReportingContext();
|
|
7800
|
+
|
|
7801
|
+
// Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
|
|
7802
|
+
const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
|
|
7803
|
+
if (!showCurrencySelector) {
|
|
7804
|
+
return null;
|
|
7805
|
+
}
|
|
7371
7806
|
return /*#__PURE__*/React__namespace.default.createElement(SingleSelect, {
|
|
7372
7807
|
items: CURRENCY_OPTIONS,
|
|
7373
7808
|
value: parameters.base_currency,
|