@bygd/nc-report-ui 0.1.40 → 0.1.42
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 +965 -571
- package/dist/default/cjs/index.cjs +966 -574
- package/dist/default/esm/index.js +966 -575
- package/package.json +1 -1
|
@@ -67,6 +67,7 @@ var LocalizationProvider = require('@mui/x-date-pickers/LocalizationProvider');
|
|
|
67
67
|
var DatePicker = require('@mui/x-date-pickers/DatePicker');
|
|
68
68
|
var AdapterDayjs = require('@mui/x-date-pickers/AdapterDayjs');
|
|
69
69
|
var dayjs = require('dayjs');
|
|
70
|
+
var CalculateIcon = require('@mui/icons-material/Calculate');
|
|
70
71
|
var styles = require('@mui/material/styles');
|
|
71
72
|
|
|
72
73
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -144,6 +145,7 @@ var BoltIcon__default = /*#__PURE__*/_interopDefault(BoltIcon);
|
|
|
144
145
|
var FilterAltIcon__default = /*#__PURE__*/_interopDefault(FilterAltIcon);
|
|
145
146
|
var InfoOutlinedIcon__default = /*#__PURE__*/_interopDefault(InfoOutlinedIcon);
|
|
146
147
|
var dayjs__default = /*#__PURE__*/_interopDefault(dayjs);
|
|
148
|
+
var CalculateIcon__default = /*#__PURE__*/_interopDefault(CalculateIcon);
|
|
147
149
|
|
|
148
150
|
function _extends() {
|
|
149
151
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -1653,6 +1655,39 @@ const ReportingProvider = ({
|
|
|
1653
1655
|
}, children);
|
|
1654
1656
|
};
|
|
1655
1657
|
|
|
1658
|
+
/**
|
|
1659
|
+
* useReportingContext - Hook to access reporting context
|
|
1660
|
+
*
|
|
1661
|
+
* @returns {Object} Context value with parameters, api, and setter functions
|
|
1662
|
+
* @returns {Object} return.parameters - Current parameters object
|
|
1663
|
+
* @returns {Function} return.setParameters - Function to update parameters
|
|
1664
|
+
* @returns {Object} return.api - Current API configuration
|
|
1665
|
+
* @returns {Function} return.setApi - Function to update API configuration
|
|
1666
|
+
* @returns {Function} return.setReportingContext - Function to update both parameters and api
|
|
1667
|
+
*
|
|
1668
|
+
* @example
|
|
1669
|
+
* const { parameters, setParameters, api, setApi, setReportingContext } = useReportingContext();
|
|
1670
|
+
*
|
|
1671
|
+
* // Update parameters only
|
|
1672
|
+
* setParameters({ base_currency: 'USD', merchants: [...] });
|
|
1673
|
+
*
|
|
1674
|
+
* // Update API only
|
|
1675
|
+
* setApi({ token: 'new-token', base_url: 'https://api.example.com' });
|
|
1676
|
+
*
|
|
1677
|
+
* // Update both at once
|
|
1678
|
+
* setReportingContext({
|
|
1679
|
+
* parameters: { base_currency: 'EUR' },
|
|
1680
|
+
* api: { token: 'token', base_url: 'https://api.example.com' }
|
|
1681
|
+
* });
|
|
1682
|
+
*/
|
|
1683
|
+
const useReportingContext = () => {
|
|
1684
|
+
const context = React.useContext(ReportingContext);
|
|
1685
|
+
if (context === undefined) {
|
|
1686
|
+
throw new Error('useReportingContext must be used within a ReportingProvider');
|
|
1687
|
+
}
|
|
1688
|
+
return context;
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1656
1691
|
/**
|
|
1657
1692
|
* useReportingContextOptional - Hook to optionally access reporting context
|
|
1658
1693
|
* Returns null if used outside of ReportingProvider (useful for optional fallback)
|
|
@@ -2574,6 +2609,140 @@ const ProviderSelection = ({
|
|
|
2574
2609
|
}, renderSelectionChain()));
|
|
2575
2610
|
};
|
|
2576
2611
|
|
|
2612
|
+
// Shared drag-and-drop primitives for the index-identified, arrow-reorderable
|
|
2613
|
+
// lists used across the report builder (Dimensions, Metrics, Column Order).
|
|
2614
|
+
// Previously duplicated per-tab; consolidated here so all three behave and
|
|
2615
|
+
// look identical and only need to be fixed in one place.
|
|
2616
|
+
|
|
2617
|
+
const useSortableListSensors = () => core.useSensors(core.useSensor(core.PointerSensor), core.useSensor(core.KeyboardSensor, {
|
|
2618
|
+
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
2619
|
+
}));
|
|
2620
|
+
|
|
2621
|
+
// Builds drag/move handlers for a list where dnd-kit item ids are the array
|
|
2622
|
+
// index. `onReorder` receives the whole reordered list.
|
|
2623
|
+
const makeListReorderHandlers = (list, onReorder) => {
|
|
2624
|
+
const handleDragEnd = ({
|
|
2625
|
+
active,
|
|
2626
|
+
over
|
|
2627
|
+
}) => {
|
|
2628
|
+
if (over && active.id !== over.id) {
|
|
2629
|
+
const oldIndex = list.findIndex((_, i) => i === active.id);
|
|
2630
|
+
const newIndex = list.findIndex((_, i) => i === over.id);
|
|
2631
|
+
onReorder(sortable.arrayMove(list, oldIndex, newIndex));
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
const handleMoveUp = index => {
|
|
2635
|
+
if (index > 0) {
|
|
2636
|
+
onReorder(sortable.arrayMove(list, index, index - 1));
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
const handleMoveDown = index => {
|
|
2640
|
+
if (index < list.length - 1) {
|
|
2641
|
+
onReorder(sortable.arrayMove(list, index, index + 1));
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
return {
|
|
2645
|
+
handleDragEnd,
|
|
2646
|
+
handleMoveUp,
|
|
2647
|
+
handleMoveDown
|
|
2648
|
+
};
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
// Wraps DndContext + SortableContext + the vertical list container.
|
|
2652
|
+
// `itemIds` must line up with the ids passed to each item's useSortable().
|
|
2653
|
+
const SortableListContext = ({
|
|
2654
|
+
sensors,
|
|
2655
|
+
itemIds,
|
|
2656
|
+
onDragEnd,
|
|
2657
|
+
children
|
|
2658
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(core.DndContext, {
|
|
2659
|
+
sensors: sensors,
|
|
2660
|
+
collisionDetection: core.closestCenter,
|
|
2661
|
+
onDragEnd: onDragEnd
|
|
2662
|
+
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
2663
|
+
items: itemIds,
|
|
2664
|
+
strategy: sortable.verticalListSortingStrategy
|
|
2665
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2666
|
+
sx: {
|
|
2667
|
+
display: "flex",
|
|
2668
|
+
flexDirection: "column",
|
|
2669
|
+
gap: 1
|
|
2670
|
+
}
|
|
2671
|
+
}, children)));
|
|
2672
|
+
|
|
2673
|
+
// Per-row useSortable() wiring shared by every sortable chip/row component.
|
|
2674
|
+
const useSortableRow = id => {
|
|
2675
|
+
const {
|
|
2676
|
+
attributes,
|
|
2677
|
+
listeners,
|
|
2678
|
+
setNodeRef,
|
|
2679
|
+
transform,
|
|
2680
|
+
transition,
|
|
2681
|
+
isDragging
|
|
2682
|
+
} = sortable.useSortable({
|
|
2683
|
+
id
|
|
2684
|
+
});
|
|
2685
|
+
const style = {
|
|
2686
|
+
transform: utilities.CSS.Transform.toString(transform),
|
|
2687
|
+
transition,
|
|
2688
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2689
|
+
display: "flex",
|
|
2690
|
+
alignItems: "center",
|
|
2691
|
+
width: "100%"
|
|
2692
|
+
};
|
|
2693
|
+
return {
|
|
2694
|
+
attributes,
|
|
2695
|
+
listeners,
|
|
2696
|
+
setNodeRef,
|
|
2697
|
+
style
|
|
2698
|
+
};
|
|
2699
|
+
};
|
|
2700
|
+
const DragHandle = ({
|
|
2701
|
+
listeners
|
|
2702
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(material.Box, _extends({}, listeners, {
|
|
2703
|
+
sx: {
|
|
2704
|
+
display: "flex",
|
|
2705
|
+
alignItems: "center",
|
|
2706
|
+
cursor: "grab",
|
|
2707
|
+
"&:active": {
|
|
2708
|
+
cursor: "grabbing"
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
2712
|
+
sx: {
|
|
2713
|
+
cursor: "grab",
|
|
2714
|
+
color: "rgba(110, 110, 110, 0.62)"
|
|
2715
|
+
}
|
|
2716
|
+
}));
|
|
2717
|
+
const MoveButtons = ({
|
|
2718
|
+
onMoveUp,
|
|
2719
|
+
onMoveDown,
|
|
2720
|
+
isFirst,
|
|
2721
|
+
isLast
|
|
2722
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2723
|
+
className: "hover-icons",
|
|
2724
|
+
sx: {
|
|
2725
|
+
display: "flex",
|
|
2726
|
+
gap: 0.5,
|
|
2727
|
+
opacity: 0,
|
|
2728
|
+
transition: "opacity 0.2s"
|
|
2729
|
+
}
|
|
2730
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2731
|
+
size: "small",
|
|
2732
|
+
onClick: onMoveUp,
|
|
2733
|
+
disabled: isFirst,
|
|
2734
|
+
"aria-label": "move up"
|
|
2735
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
2736
|
+
fontSize: "small"
|
|
2737
|
+
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2738
|
+
size: "small",
|
|
2739
|
+
onClick: onMoveDown,
|
|
2740
|
+
disabled: isLast,
|
|
2741
|
+
"aria-label": "move down"
|
|
2742
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
2743
|
+
fontSize: "small"
|
|
2744
|
+
})));
|
|
2745
|
+
|
|
2577
2746
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2578
2747
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2579
2748
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2585,6 +2754,50 @@ const interpolateTitle = (template, params) => {
|
|
|
2585
2754
|
});
|
|
2586
2755
|
};
|
|
2587
2756
|
|
|
2757
|
+
// Resolves the final column display order for the results grid: dimensions,
|
|
2758
|
+
// computed dimensions, and metrics combined into a single list.
|
|
2759
|
+
//
|
|
2760
|
+
// `columnOrder` is the optional, explicitly-arranged order (raw entries
|
|
2761
|
+
// `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
|
|
2762
|
+
// Order tab. When present, its entries come first, in that order. Anything
|
|
2763
|
+
// not covered by it (new fields added since it was last touched, or when
|
|
2764
|
+
// it's empty/undefined because no custom order has ever been defined) is
|
|
2765
|
+
// appended in the pre-existing default order: dimensions/computed (in their
|
|
2766
|
+
// own `dimensionOrder`) first, then metrics (in their own array order) —
|
|
2767
|
+
// this keeps behavior unchanged for reports that never define a column order.
|
|
2768
|
+
const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
2769
|
+
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2770
|
+
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2771
|
+
// `.key` regardless of where the item came from.
|
|
2772
|
+
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2773
|
+
kind: item.kind,
|
|
2774
|
+
key: item.kind === 'computed' ? item.data.name : item.data.fullPath,
|
|
2775
|
+
data: item.data
|
|
2776
|
+
}));
|
|
2777
|
+
const normalizedMetricItems = metrics.map(m => ({
|
|
2778
|
+
kind: 'metric',
|
|
2779
|
+
key: m.fullPath,
|
|
2780
|
+
data: m
|
|
2781
|
+
}));
|
|
2782
|
+
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
2783
|
+
const resolved = [];
|
|
2784
|
+
const seen = new Set();
|
|
2785
|
+
(columnOrder || []).forEach(entry => {
|
|
2786
|
+
const seenKey = `${entry.type}:${entry.key}`;
|
|
2787
|
+
if (seen.has(seenKey) || !byKey[seenKey]) return;
|
|
2788
|
+
resolved.push(byKey[seenKey]);
|
|
2789
|
+
seen.add(seenKey);
|
|
2790
|
+
});
|
|
2791
|
+
[...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
|
|
2792
|
+
const seenKey = `${item.kind}:${item.key}`;
|
|
2793
|
+
if (!seen.has(seenKey)) {
|
|
2794
|
+
resolved.push(item);
|
|
2795
|
+
seen.add(seenKey);
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
return resolved;
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2588
2801
|
// Sortable Chip Component
|
|
2589
2802
|
const SortableChip$1 = ({
|
|
2590
2803
|
id,
|
|
@@ -2613,20 +2826,8 @@ const SortableChip$1 = ({
|
|
|
2613
2826
|
attributes,
|
|
2614
2827
|
listeners,
|
|
2615
2828
|
setNodeRef,
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
isDragging
|
|
2619
|
-
} = sortable.useSortable({
|
|
2620
|
-
id
|
|
2621
|
-
});
|
|
2622
|
-
const style = {
|
|
2623
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
2624
|
-
transition,
|
|
2625
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2626
|
-
display: "flex",
|
|
2627
|
-
alignItems: "center",
|
|
2628
|
-
width: "100%"
|
|
2629
|
-
};
|
|
2829
|
+
style
|
|
2830
|
+
} = useSortableRow(id);
|
|
2630
2831
|
|
|
2631
2832
|
// Focus input when entering edit mode
|
|
2632
2833
|
React.useEffect(() => {
|
|
@@ -2735,21 +2936,9 @@ const SortableChip$1 = ({
|
|
|
2735
2936
|
opacity: 1 // show icons on hover
|
|
2736
2937
|
}
|
|
2737
2938
|
}
|
|
2738
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
alignItems: "center",
|
|
2742
|
-
cursor: "grab",
|
|
2743
|
-
"&:active": {
|
|
2744
|
-
cursor: "grabbing"
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
2748
|
-
sx: {
|
|
2749
|
-
cursor: "grab",
|
|
2750
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2751
|
-
}
|
|
2752
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2939
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
2940
|
+
listeners: listeners
|
|
2941
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2753
2942
|
sx: {
|
|
2754
2943
|
minWidth: 0
|
|
2755
2944
|
}
|
|
@@ -2812,29 +3001,12 @@ const SortableChip$1 = ({
|
|
|
2812
3001
|
sx: {
|
|
2813
3002
|
flex: 1
|
|
2814
3003
|
}
|
|
2815
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
transition: "opacity 0.2s"
|
|
2822
|
-
}
|
|
2823
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2824
|
-
size: "small",
|
|
2825
|
-
onClick: onMoveUp,
|
|
2826
|
-
disabled: isFirst,
|
|
2827
|
-
"aria-label": "move up"
|
|
2828
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
2829
|
-
fontSize: "small"
|
|
2830
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2831
|
-
size: "small",
|
|
2832
|
-
onClick: onMoveDown,
|
|
2833
|
-
disabled: isLast,
|
|
2834
|
-
"aria-label": "move down"
|
|
2835
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
2836
|
-
fontSize: "small"
|
|
2837
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3004
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
3005
|
+
onMoveUp: onMoveUp,
|
|
3006
|
+
onMoveDown: onMoveDown,
|
|
3007
|
+
isFirst: isFirst,
|
|
3008
|
+
isLast: isLast
|
|
3009
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
2838
3010
|
inputRef: inputRef,
|
|
2839
3011
|
value: editValue,
|
|
2840
3012
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -2896,7 +3068,6 @@ const SortableChip$1 = ({
|
|
|
2896
3068
|
// the top-of-page Add form, so it stays visible regardless of scroll position.
|
|
2897
3069
|
const SortableComputedChip = ({
|
|
2898
3070
|
id,
|
|
2899
|
-
index,
|
|
2900
3071
|
name,
|
|
2901
3072
|
value,
|
|
2902
3073
|
onDelete,
|
|
@@ -2911,24 +3082,12 @@ const SortableComputedChip = ({
|
|
|
2911
3082
|
attributes,
|
|
2912
3083
|
listeners,
|
|
2913
3084
|
setNodeRef,
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
isDragging
|
|
2917
|
-
} = sortable.useSortable({
|
|
2918
|
-
id
|
|
2919
|
-
});
|
|
3085
|
+
style
|
|
3086
|
+
} = useSortableRow(id);
|
|
2920
3087
|
const [isEditing, setIsEditing] = React.useState(false);
|
|
2921
3088
|
const [editName, setEditName] = React.useState("");
|
|
2922
3089
|
const [editValue, setEditValue] = React.useState("");
|
|
2923
3090
|
const containerRef = React.useRef(null);
|
|
2924
|
-
const style = {
|
|
2925
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
2926
|
-
transition,
|
|
2927
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2928
|
-
display: "flex",
|
|
2929
|
-
alignItems: "center",
|
|
2930
|
-
width: "100%"
|
|
2931
|
-
};
|
|
2932
3091
|
|
|
2933
3092
|
// Handle click outside to cancel
|
|
2934
3093
|
React.useEffect(() => {
|
|
@@ -2949,13 +3108,13 @@ const SortableComputedChip = ({
|
|
|
2949
3108
|
setEditValue(value);
|
|
2950
3109
|
setIsEditing(true);
|
|
2951
3110
|
};
|
|
2952
|
-
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(),
|
|
3111
|
+
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), name);
|
|
2953
3112
|
const handleSave = () => {
|
|
2954
3113
|
const trimmedName = editName.trim();
|
|
2955
3114
|
const trimmedValue = editValue.trim();
|
|
2956
3115
|
if (!trimmedName || !trimmedValue) return;
|
|
2957
|
-
if (isNameTaken(trimmedName,
|
|
2958
|
-
onUpdate(
|
|
3116
|
+
if (isNameTaken(trimmedName, name)) return;
|
|
3117
|
+
onUpdate(name, {
|
|
2959
3118
|
name: trimmedName,
|
|
2960
3119
|
value: trimmedValue
|
|
2961
3120
|
});
|
|
@@ -2991,21 +3150,9 @@ const SortableComputedChip = ({
|
|
|
2991
3150
|
opacity: 1
|
|
2992
3151
|
}
|
|
2993
3152
|
}
|
|
2994
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
alignItems: "center",
|
|
2998
|
-
cursor: "grab",
|
|
2999
|
-
"&:active": {
|
|
3000
|
-
cursor: "grabbing"
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
3004
|
-
sx: {
|
|
3005
|
-
cursor: "grab",
|
|
3006
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3007
|
-
}
|
|
3008
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
3153
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
3154
|
+
listeners: listeners
|
|
3155
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
3009
3156
|
icon: /*#__PURE__*/React__namespace.default.createElement(FunctionsIcon__default.default, {
|
|
3010
3157
|
sx: {
|
|
3011
3158
|
fontSize: "15px !important"
|
|
@@ -3069,29 +3216,12 @@ const SortableComputedChip = ({
|
|
|
3069
3216
|
sx: {
|
|
3070
3217
|
flex: 1
|
|
3071
3218
|
}
|
|
3072
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
transition: "opacity 0.2s"
|
|
3079
|
-
}
|
|
3080
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3081
|
-
size: "small",
|
|
3082
|
-
onClick: onMoveUp,
|
|
3083
|
-
disabled: isFirst,
|
|
3084
|
-
"aria-label": "move up"
|
|
3085
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
3086
|
-
fontSize: "small"
|
|
3087
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3088
|
-
size: "small",
|
|
3089
|
-
onClick: onMoveDown,
|
|
3090
|
-
disabled: isLast,
|
|
3091
|
-
"aria-label": "move down"
|
|
3092
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
3093
|
-
fontSize: "small"
|
|
3094
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3219
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
3220
|
+
onMoveUp: onMoveUp,
|
|
3221
|
+
onMoveDown: onMoveDown,
|
|
3222
|
+
isFirst: isFirst,
|
|
3223
|
+
isLast: isLast
|
|
3224
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3095
3225
|
value: editName,
|
|
3096
3226
|
onChange: e => setEditName(e.target.value),
|
|
3097
3227
|
onKeyDown: handleKeyDown,
|
|
@@ -3153,7 +3283,7 @@ const Dimensions = ({
|
|
|
3153
3283
|
savedDimensions = [],
|
|
3154
3284
|
onSaveDimension,
|
|
3155
3285
|
onRemoveDimension,
|
|
3156
|
-
|
|
3286
|
+
onUpdateDimensionSortOrder,
|
|
3157
3287
|
titleOverrides = {},
|
|
3158
3288
|
onUpdateTitle,
|
|
3159
3289
|
onResetTitle,
|
|
@@ -3163,7 +3293,8 @@ const Dimensions = ({
|
|
|
3163
3293
|
onSaveComputedDimension,
|
|
3164
3294
|
onUpdateComputedDimension,
|
|
3165
3295
|
onRemoveComputedDimension,
|
|
3166
|
-
|
|
3296
|
+
dimensionOrder = [],
|
|
3297
|
+
onReorderDimensionItems
|
|
3167
3298
|
}) => {
|
|
3168
3299
|
const reportingContext = useReportingContextOptional();
|
|
3169
3300
|
const parameters = reportingContext?.parameters;
|
|
@@ -3175,53 +3306,38 @@ const Dimensions = ({
|
|
|
3175
3306
|
const [computedValue, setComputedValue] = React.useState("");
|
|
3176
3307
|
|
|
3177
3308
|
// Setup drag and drop sensors
|
|
3178
|
-
const sensors =
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
//
|
|
3183
|
-
const
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
}
|
|
3202
|
-
};
|
|
3203
|
-
|
|
3204
|
-
// Handle move down
|
|
3205
|
-
const handleMoveDown = index => {
|
|
3206
|
-
if (index < savedDimensions.length - 1) {
|
|
3207
|
-
const newOrder = sortable.arrayMove(savedDimensions, index, index + 1);
|
|
3208
|
-
onReorderDimensions(newOrder);
|
|
3209
|
-
}
|
|
3210
|
-
};
|
|
3309
|
+
const sensors = useSortableListSensors();
|
|
3310
|
+
|
|
3311
|
+
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3312
|
+
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3313
|
+
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3314
|
+
const dimensionByFullPath = Object.fromEntries(savedDimensions.map(d => [d.fullPath, d]));
|
|
3315
|
+
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3316
|
+
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3317
|
+
kind: "dimension",
|
|
3318
|
+
key: entry.key,
|
|
3319
|
+
data: dimensionByFullPath[entry.key]
|
|
3320
|
+
} : {
|
|
3321
|
+
kind: "computed",
|
|
3322
|
+
key: entry.key,
|
|
3323
|
+
data: computedDimensionByName[entry.key]
|
|
3324
|
+
}).filter(item => item.data);
|
|
3325
|
+
|
|
3326
|
+
// Handle drag/move for the unified list
|
|
3327
|
+
const {
|
|
3328
|
+
handleDragEnd,
|
|
3329
|
+
handleMoveUp,
|
|
3330
|
+
handleMoveDown
|
|
3331
|
+
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3211
3332
|
|
|
3212
|
-
// Handle sort order change
|
|
3213
|
-
const handleSortOrderChange = (
|
|
3214
|
-
|
|
3215
|
-
newDimensions[index] = {
|
|
3216
|
-
...newDimensions[index],
|
|
3217
|
-
sortOrder
|
|
3218
|
-
};
|
|
3219
|
-
onReorderDimensions(newDimensions);
|
|
3333
|
+
// Handle sort order change (dimensions only - computed dimensions have no order_by)
|
|
3334
|
+
const handleSortOrderChange = (fullPath, sortOrder) => {
|
|
3335
|
+
onUpdateDimensionSortOrder(fullPath, sortOrder);
|
|
3220
3336
|
};
|
|
3221
3337
|
|
|
3222
3338
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
3223
3339
|
// Their `value` is a raw SQL literal/expression sent to the API as-is.
|
|
3224
|
-
const isComputedNameTaken = (name,
|
|
3340
|
+
const isComputedNameTaken = (name, excludeName = null) => savedComputedDimensions.some(cd => cd.name === name) && name !== excludeName;
|
|
3225
3341
|
const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
|
|
3226
3342
|
const handleAddComputedClick = () => {
|
|
3227
3343
|
setIsAddingComputed(true);
|
|
@@ -3245,27 +3361,6 @@ const Dimensions = ({
|
|
|
3245
3361
|
});
|
|
3246
3362
|
handleCancelComputed();
|
|
3247
3363
|
};
|
|
3248
|
-
const handleComputedDragEnd = event => {
|
|
3249
|
-
const {
|
|
3250
|
-
active,
|
|
3251
|
-
over
|
|
3252
|
-
} = event;
|
|
3253
|
-
if (over && active.id !== over.id) {
|
|
3254
|
-
const oldIndex = savedComputedDimensions.findIndex((_, i) => i === active.id);
|
|
3255
|
-
const newIndex = savedComputedDimensions.findIndex((_, i) => i === over.id);
|
|
3256
|
-
onReorderComputedDimensions(sortable.arrayMove(savedComputedDimensions, oldIndex, newIndex));
|
|
3257
|
-
}
|
|
3258
|
-
};
|
|
3259
|
-
const handleMoveComputedUp = index => {
|
|
3260
|
-
if (index > 0) {
|
|
3261
|
-
onReorderComputedDimensions(sortable.arrayMove(savedComputedDimensions, index, index - 1));
|
|
3262
|
-
}
|
|
3263
|
-
};
|
|
3264
|
-
const handleMoveComputedDown = index => {
|
|
3265
|
-
if (index < savedComputedDimensions.length - 1) {
|
|
3266
|
-
onReorderComputedDimensions(sortable.arrayMove(savedComputedDimensions, index, index + 1));
|
|
3267
|
-
}
|
|
3268
|
-
};
|
|
3269
3364
|
|
|
3270
3365
|
// Get the current provider based on selection chain
|
|
3271
3366
|
const getCurrentProvider = () => {
|
|
@@ -3708,7 +3803,7 @@ const Dimensions = ({
|
|
|
3708
3803
|
boxShadow: "none"
|
|
3709
3804
|
}
|
|
3710
3805
|
}
|
|
3711
|
-
}, "Save Dimension"))),
|
|
3806
|
+
}, "Save Dimension"))), orderedItems.length > 0 && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3712
3807
|
sx: {
|
|
3713
3808
|
marginTop: 0
|
|
3714
3809
|
}
|
|
@@ -3726,81 +3821,40 @@ const Dimensions = ({
|
|
|
3726
3821
|
color: "#666",
|
|
3727
3822
|
marginBottom: 2
|
|
3728
3823
|
}
|
|
3729
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(
|
|
3824
|
+
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
3730
3825
|
sensors: sensors,
|
|
3731
|
-
|
|
3826
|
+
itemIds: orderedItems.map((_, index) => index),
|
|
3732
3827
|
onDragEnd: handleDragEnd
|
|
3733
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
3734
|
-
|
|
3735
|
-
strategy: sortable.verticalListSortingStrategy
|
|
3736
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3737
|
-
sx: {
|
|
3738
|
-
display: "flex",
|
|
3739
|
-
flexDirection: "column",
|
|
3740
|
-
gap: 1
|
|
3741
|
-
}
|
|
3742
|
-
}, savedDimensions.map((dim, index) => /*#__PURE__*/React__namespace.default.createElement(SortableChip$1, {
|
|
3743
|
-
key: index,
|
|
3828
|
+
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__namespace.default.createElement(SortableChip$1, {
|
|
3829
|
+
key: `dim-${item.key}`,
|
|
3744
3830
|
id: index,
|
|
3745
|
-
label:
|
|
3746
|
-
fullLabel: `${formatProviderPath(
|
|
3747
|
-
onDelete: () => onRemoveDimension(
|
|
3831
|
+
label: item.data.dimensionTitle,
|
|
3832
|
+
fullLabel: `${formatProviderPath(item.data)} → ${item.data.dimensionTitle} (${item.data.fullPath})`,
|
|
3833
|
+
onDelete: () => onRemoveDimension(item.key),
|
|
3748
3834
|
onMoveUp: () => handleMoveUp(index),
|
|
3749
3835
|
onMoveDown: () => handleMoveDown(index),
|
|
3750
3836
|
isFirst: index === 0,
|
|
3751
|
-
isLast: index ===
|
|
3752
|
-
sortOrder:
|
|
3753
|
-
onSortOrderChange: sortOrder => handleSortOrderChange(
|
|
3754
|
-
fullPath:
|
|
3755
|
-
defaultTitle:
|
|
3756
|
-
customTitle: titleOverrides[
|
|
3837
|
+
isLast: index === orderedItems.length - 1,
|
|
3838
|
+
sortOrder: item.data.sortOrder || null,
|
|
3839
|
+
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3840
|
+
fullPath: item.data.fullPath,
|
|
3841
|
+
defaultTitle: item.data.dimensionTitle,
|
|
3842
|
+
customTitle: titleOverrides[item.data.fullPath],
|
|
3757
3843
|
onUpdateTitle: onUpdateTitle,
|
|
3758
3844
|
onResetTitle: onResetTitle
|
|
3759
|
-
})
|
|
3760
|
-
|
|
3761
|
-
marginTop: 2
|
|
3762
|
-
}
|
|
3763
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
3764
|
-
variant: "h6",
|
|
3765
|
-
sx: {
|
|
3766
|
-
fontSize: "16px",
|
|
3767
|
-
fontWeight: 600,
|
|
3768
|
-
marginTop: 2,
|
|
3769
|
-
color: "rgb(37, 37, 37)"
|
|
3770
|
-
}
|
|
3771
|
-
}, "Saved Computed Dimensions"), /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
3772
|
-
sx: {
|
|
3773
|
-
fontSize: "13px",
|
|
3774
|
-
color: "#666",
|
|
3775
|
-
marginBottom: 2
|
|
3776
|
-
}
|
|
3777
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(core.DndContext, {
|
|
3778
|
-
sensors: sensors,
|
|
3779
|
-
collisionDetection: core.closestCenter,
|
|
3780
|
-
onDragEnd: handleComputedDragEnd
|
|
3781
|
-
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
3782
|
-
items: savedComputedDimensions.map((_, index) => index),
|
|
3783
|
-
strategy: sortable.verticalListSortingStrategy
|
|
3784
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3785
|
-
sx: {
|
|
3786
|
-
display: "flex",
|
|
3787
|
-
flexDirection: "column",
|
|
3788
|
-
gap: 1
|
|
3789
|
-
}
|
|
3790
|
-
}, savedComputedDimensions.map((cd, index) => /*#__PURE__*/React__namespace.default.createElement(SortableComputedChip, {
|
|
3791
|
-
key: index,
|
|
3845
|
+
}) : /*#__PURE__*/React__namespace.default.createElement(SortableComputedChip, {
|
|
3846
|
+
key: `comp-${item.key}`,
|
|
3792
3847
|
id: index,
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
value: cd.value,
|
|
3848
|
+
name: item.data.name,
|
|
3849
|
+
value: item.data.value,
|
|
3796
3850
|
onUpdate: onUpdateComputedDimension,
|
|
3797
3851
|
isNameTaken: isComputedNameTaken,
|
|
3798
|
-
onDelete: () => onRemoveComputedDimension(
|
|
3799
|
-
onMoveUp: () =>
|
|
3800
|
-
onMoveDown: () =>
|
|
3852
|
+
onDelete: () => onRemoveComputedDimension(item.key),
|
|
3853
|
+
onMoveUp: () => handleMoveUp(index),
|
|
3854
|
+
onMoveDown: () => handleMoveDown(index),
|
|
3801
3855
|
isFirst: index === 0,
|
|
3802
|
-
isLast: index ===
|
|
3803
|
-
})))))
|
|
3856
|
+
isLast: index === orderedItems.length - 1
|
|
3857
|
+
})))));
|
|
3804
3858
|
};
|
|
3805
3859
|
|
|
3806
3860
|
const MetricSourceIcon = ({
|
|
@@ -3875,20 +3929,8 @@ const SortableChip = ({
|
|
|
3875
3929
|
attributes,
|
|
3876
3930
|
listeners,
|
|
3877
3931
|
setNodeRef,
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
isDragging
|
|
3881
|
-
} = sortable.useSortable({
|
|
3882
|
-
id
|
|
3883
|
-
});
|
|
3884
|
-
const style = {
|
|
3885
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
3886
|
-
transition,
|
|
3887
|
-
opacity: isDragging ? 0.5 : 1,
|
|
3888
|
-
display: 'flex',
|
|
3889
|
-
alignItems: 'center',
|
|
3890
|
-
width: '100%'
|
|
3891
|
-
};
|
|
3932
|
+
style
|
|
3933
|
+
} = useSortableRow(id);
|
|
3892
3934
|
|
|
3893
3935
|
// Focus input when entering edit mode
|
|
3894
3936
|
React.useEffect(() => {
|
|
@@ -3966,21 +4008,9 @@ const SortableChip = ({
|
|
|
3966
4008
|
opacity: 1 // show icons on hover
|
|
3967
4009
|
}
|
|
3968
4010
|
}
|
|
3969
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
alignItems: "center",
|
|
3973
|
-
cursor: "grab",
|
|
3974
|
-
"&:active": {
|
|
3975
|
-
cursor: "grabbing"
|
|
3976
|
-
}
|
|
3977
|
-
}
|
|
3978
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
3979
|
-
sx: {
|
|
3980
|
-
cursor: "grab",
|
|
3981
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3982
|
-
}
|
|
3983
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, source && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4011
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
4012
|
+
listeners: listeners
|
|
4013
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, source && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3984
4014
|
sx: {
|
|
3985
4015
|
display: 'flex',
|
|
3986
4016
|
alignItems: 'center',
|
|
@@ -4042,29 +4072,12 @@ const SortableChip = ({
|
|
|
4042
4072
|
sx: {
|
|
4043
4073
|
flex: 1
|
|
4044
4074
|
}
|
|
4045
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
transition: "opacity 0.2s"
|
|
4052
|
-
}
|
|
4053
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4054
|
-
size: "small",
|
|
4055
|
-
onClick: onMoveUp,
|
|
4056
|
-
disabled: isFirst,
|
|
4057
|
-
"aria-label": "move up"
|
|
4058
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
4059
|
-
fontSize: "small"
|
|
4060
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4061
|
-
size: "small",
|
|
4062
|
-
onClick: onMoveDown,
|
|
4063
|
-
disabled: isLast,
|
|
4064
|
-
"aria-label": "move down"
|
|
4065
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
4066
|
-
fontSize: "small"
|
|
4067
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4075
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
4076
|
+
onMoveUp: onMoveUp,
|
|
4077
|
+
onMoveDown: onMoveDown,
|
|
4078
|
+
isFirst: isFirst,
|
|
4079
|
+
isLast: isLast
|
|
4080
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4068
4081
|
inputRef: inputRef,
|
|
4069
4082
|
value: editValue,
|
|
4070
4083
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -4139,39 +4152,14 @@ const Metrics = ({
|
|
|
4139
4152
|
const [selectedMetric, setSelectedMetric] = React.useState('');
|
|
4140
4153
|
|
|
4141
4154
|
// Setup drag and drop sensors
|
|
4142
|
-
const sensors =
|
|
4143
|
-
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
4144
|
-
}));
|
|
4145
|
-
|
|
4146
|
-
// Handle drag end
|
|
4147
|
-
const handleDragEnd = event => {
|
|
4148
|
-
const {
|
|
4149
|
-
active,
|
|
4150
|
-
over
|
|
4151
|
-
} = event;
|
|
4152
|
-
if (over && active.id !== over.id) {
|
|
4153
|
-
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
4154
|
-
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
4155
|
-
const newOrder = sortable.arrayMove(savedMetrics, oldIndex, newIndex);
|
|
4156
|
-
onReorderMetrics(newOrder);
|
|
4157
|
-
}
|
|
4158
|
-
};
|
|
4159
|
-
|
|
4160
|
-
// Handle move up
|
|
4161
|
-
const handleMoveUp = index => {
|
|
4162
|
-
if (index > 0) {
|
|
4163
|
-
const newOrder = sortable.arrayMove(savedMetrics, index, index - 1);
|
|
4164
|
-
onReorderMetrics(newOrder);
|
|
4165
|
-
}
|
|
4166
|
-
};
|
|
4155
|
+
const sensors = useSortableListSensors();
|
|
4167
4156
|
|
|
4168
|
-
// Handle move
|
|
4169
|
-
const
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
};
|
|
4157
|
+
// Handle drag/move
|
|
4158
|
+
const {
|
|
4159
|
+
handleDragEnd,
|
|
4160
|
+
handleMoveUp,
|
|
4161
|
+
handleMoveDown
|
|
4162
|
+
} = makeListReorderHandlers(savedMetrics, onReorderMetrics);
|
|
4175
4163
|
|
|
4176
4164
|
// Get the current provider based on selection chain
|
|
4177
4165
|
const getCurrentProvider = () => {
|
|
@@ -4510,19 +4498,10 @@ const Metrics = ({
|
|
|
4510
4498
|
marginBottom: 1,
|
|
4511
4499
|
color: "rgb(37, 37, 37)"
|
|
4512
4500
|
}
|
|
4513
|
-
}, "Saved Metrics"), /*#__PURE__*/React__namespace.default.createElement(
|
|
4501
|
+
}, "Saved Metrics"), /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
4514
4502
|
sensors: sensors,
|
|
4515
|
-
|
|
4503
|
+
itemIds: savedMetrics.map((_, index) => index),
|
|
4516
4504
|
onDragEnd: handleDragEnd
|
|
4517
|
-
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
4518
|
-
items: savedMetrics.map((_, index) => index),
|
|
4519
|
-
strategy: sortable.verticalListSortingStrategy
|
|
4520
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4521
|
-
sx: {
|
|
4522
|
-
display: 'flex',
|
|
4523
|
-
flexDirection: 'column',
|
|
4524
|
-
gap: 1
|
|
4525
|
-
}
|
|
4526
4505
|
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__namespace.default.createElement(SortableChip, {
|
|
4527
4506
|
key: index,
|
|
4528
4507
|
id: index,
|
|
@@ -4539,7 +4518,7 @@ const Metrics = ({
|
|
|
4539
4518
|
onUpdateTitle: onUpdateTitle,
|
|
4540
4519
|
onResetTitle: onResetTitle,
|
|
4541
4520
|
source: metric.metric?.source
|
|
4542
|
-
})))))
|
|
4521
|
+
})))));
|
|
4543
4522
|
};
|
|
4544
4523
|
|
|
4545
4524
|
const Filters = ({
|
|
@@ -5766,9 +5745,208 @@ const Filters = ({
|
|
|
5766
5745
|
}))));
|
|
5767
5746
|
};
|
|
5768
5747
|
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5748
|
+
const KIND_LABEL = {
|
|
5749
|
+
dimension: "Dimension",
|
|
5750
|
+
computed: "Computed",
|
|
5751
|
+
metric: "Metric"
|
|
5752
|
+
};
|
|
5753
|
+
const KindChip = ({
|
|
5754
|
+
kind,
|
|
5755
|
+
source
|
|
5756
|
+
}) => {
|
|
5757
|
+
if (kind === "metric") {
|
|
5758
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5759
|
+
sx: {
|
|
5760
|
+
display: "flex",
|
|
5761
|
+
alignItems: "center",
|
|
5762
|
+
flexShrink: 0
|
|
5763
|
+
}
|
|
5764
|
+
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
5765
|
+
source: source
|
|
5766
|
+
}));
|
|
5767
|
+
}
|
|
5768
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
5769
|
+
icon: kind === "computed" ? /*#__PURE__*/React__namespace.default.createElement(CalculateIcon__default.default, {
|
|
5770
|
+
sx: {
|
|
5771
|
+
fontSize: "15px !important"
|
|
5772
|
+
}
|
|
5773
|
+
}) : undefined,
|
|
5774
|
+
label: KIND_LABEL[kind],
|
|
5775
|
+
size: "small",
|
|
5776
|
+
sx: {
|
|
5777
|
+
fontWeight: 500,
|
|
5778
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
5779
|
+
color: "rgb(70, 134, 128)",
|
|
5780
|
+
flexShrink: 0
|
|
5781
|
+
}
|
|
5782
|
+
});
|
|
5783
|
+
};
|
|
5784
|
+
|
|
5785
|
+
// Read-only sortable row: this tab only reorders columns. Adding, removing,
|
|
5786
|
+
// and renaming stay on the Dimensions/Metrics tabs so there's one place each
|
|
5787
|
+
// field is managed.
|
|
5788
|
+
const SortableColumnRow = ({
|
|
5789
|
+
id,
|
|
5790
|
+
label,
|
|
5791
|
+
fullPath,
|
|
5792
|
+
kind,
|
|
5793
|
+
source,
|
|
5794
|
+
onMoveUp,
|
|
5795
|
+
onMoveDown,
|
|
5796
|
+
isFirst,
|
|
5797
|
+
isLast
|
|
5798
|
+
}) => {
|
|
5799
|
+
const reportingContext = useReportingContextOptional();
|
|
5800
|
+
const parameters = reportingContext?.parameters;
|
|
5801
|
+
const {
|
|
5802
|
+
attributes,
|
|
5803
|
+
listeners,
|
|
5804
|
+
setNodeRef,
|
|
5805
|
+
style
|
|
5806
|
+
} = useSortableRow(id);
|
|
5807
|
+
const displayLabel = interpolateTitle(label, parameters);
|
|
5808
|
+
return /*#__PURE__*/React__namespace.default.createElement("div", _extends({
|
|
5809
|
+
ref: setNodeRef,
|
|
5810
|
+
style: style
|
|
5811
|
+
}, attributes), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5812
|
+
sx: {
|
|
5813
|
+
display: "flex",
|
|
5814
|
+
alignItems: "center",
|
|
5815
|
+
width: "100%",
|
|
5816
|
+
gap: 1,
|
|
5817
|
+
backgroundColor: "white",
|
|
5818
|
+
border: "1px solid #e0e0e0",
|
|
5819
|
+
borderRadius: 2,
|
|
5820
|
+
padding: 1,
|
|
5821
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
5822
|
+
"&:hover .hover-icons": {
|
|
5823
|
+
opacity: 1
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
5827
|
+
listeners: listeners
|
|
5828
|
+
}), /*#__PURE__*/React__namespace.default.createElement(KindChip, {
|
|
5829
|
+
kind: kind,
|
|
5830
|
+
source: source
|
|
5831
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Tooltip, {
|
|
5832
|
+
title: fullPath,
|
|
5833
|
+
arrow: true,
|
|
5834
|
+
placement: "top"
|
|
5835
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5836
|
+
variant: "h6",
|
|
5837
|
+
sx: {
|
|
5838
|
+
fontWeight: 500,
|
|
5839
|
+
color: "#1a1a1a",
|
|
5840
|
+
fontSize: "14px",
|
|
5841
|
+
whiteSpace: "nowrap",
|
|
5842
|
+
overflow: "hidden",
|
|
5843
|
+
textOverflow: "ellipsis"
|
|
5844
|
+
}
|
|
5845
|
+
}, displayLabel)), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5846
|
+
sx: {
|
|
5847
|
+
flex: 1
|
|
5848
|
+
}
|
|
5849
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
5850
|
+
onMoveUp: onMoveUp,
|
|
5851
|
+
onMoveDown: onMoveDown,
|
|
5852
|
+
isFirst: isFirst,
|
|
5853
|
+
isLast: isLast
|
|
5854
|
+
})));
|
|
5855
|
+
};
|
|
5856
|
+
const ColumnOrder = ({
|
|
5857
|
+
orderedDimensionItems = [],
|
|
5858
|
+
metrics = [],
|
|
5859
|
+
columnOrder = [],
|
|
5860
|
+
onReorderColumnItems,
|
|
5861
|
+
titleOverrides = {
|
|
5862
|
+
dimensions: {},
|
|
5863
|
+
metrics: {}
|
|
5864
|
+
}
|
|
5865
|
+
}) => {
|
|
5866
|
+
const isCustomized = columnOrder.length > 0;
|
|
5867
|
+
const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
|
|
5868
|
+
const sensors = useSortableListSensors();
|
|
5869
|
+
const reorderResolved = newResolvedItems => {
|
|
5870
|
+
onReorderColumnItems(newResolvedItems.map(item => ({
|
|
5871
|
+
type: item.kind,
|
|
5872
|
+
key: item.key
|
|
5873
|
+
})));
|
|
5874
|
+
};
|
|
5875
|
+
const {
|
|
5876
|
+
handleDragEnd,
|
|
5877
|
+
handleMoveUp,
|
|
5878
|
+
handleMoveDown
|
|
5879
|
+
} = makeListReorderHandlers(resolvedItems, reorderResolved);
|
|
5880
|
+
const labelFor = item => {
|
|
5881
|
+
if (item.kind === "computed") return item.data.name;
|
|
5882
|
+
if (item.kind === "metric") {
|
|
5883
|
+
return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
|
|
5884
|
+
}
|
|
5885
|
+
return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
|
|
5886
|
+
};
|
|
5887
|
+
return /*#__PURE__*/React__namespace.default.createElement("div", null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5888
|
+
sx: {
|
|
5889
|
+
display: "flex",
|
|
5890
|
+
justifyContent: "space-between",
|
|
5891
|
+
alignItems: "flex-start",
|
|
5892
|
+
mb: 2,
|
|
5893
|
+
gap: 2
|
|
5894
|
+
}
|
|
5895
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5896
|
+
sx: {
|
|
5897
|
+
fontSize: "13px",
|
|
5898
|
+
color: "#666"
|
|
5899
|
+
}
|
|
5900
|
+
}, "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, {
|
|
5901
|
+
title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
|
|
5902
|
+
arrow: true
|
|
5903
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
5904
|
+
variant: "outlined",
|
|
5905
|
+
size: "small",
|
|
5906
|
+
startIcon: /*#__PURE__*/React__namespace.default.createElement(RestartAltIcon__default.default, null),
|
|
5907
|
+
onClick: () => onReorderColumnItems([]),
|
|
5908
|
+
sx: {
|
|
5909
|
+
height: "36px",
|
|
5910
|
+
flexShrink: 0,
|
|
5911
|
+
fontFamily: "system-ui",
|
|
5912
|
+
fontSize: "13px",
|
|
5913
|
+
fontWeight: 500,
|
|
5914
|
+
borderRadius: "8px",
|
|
5915
|
+
boxShadow: "none",
|
|
5916
|
+
textTransform: "none",
|
|
5917
|
+
borderColor: "#e0e0e0",
|
|
5918
|
+
color: "rgb(37, 37, 37)",
|
|
5919
|
+
"&:hover": {
|
|
5920
|
+
backgroundColor: "#f5f5f5",
|
|
5921
|
+
borderColor: "#d0d0d0"
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
}, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5925
|
+
sx: {
|
|
5926
|
+
fontSize: "13px",
|
|
5927
|
+
color: "#999"
|
|
5928
|
+
}
|
|
5929
|
+
}, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
5930
|
+
sensors: sensors,
|
|
5931
|
+
itemIds: resolvedItems.map((_, index) => index),
|
|
5932
|
+
onDragEnd: handleDragEnd
|
|
5933
|
+
}, resolvedItems.map((item, index) => /*#__PURE__*/React__namespace.default.createElement(SortableColumnRow, {
|
|
5934
|
+
key: `${item.kind}-${item.key}`,
|
|
5935
|
+
id: index,
|
|
5936
|
+
label: labelFor(item),
|
|
5937
|
+
fullPath: item.kind === "computed" ? item.data.value : item.key,
|
|
5938
|
+
kind: item.kind,
|
|
5939
|
+
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
5940
|
+
onMoveUp: () => handleMoveUp(index),
|
|
5941
|
+
onMoveDown: () => handleMoveDown(index),
|
|
5942
|
+
isFirst: index === 0,
|
|
5943
|
+
isLast: index === resolvedItems.length - 1
|
|
5944
|
+
}))));
|
|
5945
|
+
};
|
|
5946
|
+
|
|
5947
|
+
// Default numeral.js formats applied when a dimension/metric definition has a
|
|
5948
|
+
// known type but no explicit `format` set on the provider.
|
|
5949
|
+
const DEFAULT_FORMATS_BY_TYPE = {
|
|
5772
5950
|
currency: '0,0.00',
|
|
5773
5951
|
integer: '0,0'
|
|
5774
5952
|
};
|
|
@@ -5780,11 +5958,107 @@ const formatValue = (value, def) => {
|
|
|
5780
5958
|
return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
|
|
5781
5959
|
};
|
|
5782
5960
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5961
|
+
|
|
5962
|
+
// Builds a single DataGrid column definition for one resolved column item
|
|
5963
|
+
// (a dimension, computed dimension, or metric). Field naming logic:
|
|
5964
|
+
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
5965
|
+
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5966
|
+
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
5967
|
+
// each row keyed by the computed dimension's own `name`.
|
|
5968
|
+
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
5969
|
+
// (see comment inline below), normalised to match the row-key normalisation
|
|
5970
|
+
// done when building `rows`.
|
|
5971
|
+
const buildColumn = (item, titleOverrides, parameters) => {
|
|
5972
|
+
if (item.kind === 'computed') {
|
|
5973
|
+
const cd = item.data;
|
|
5974
|
+
return {
|
|
5975
|
+
field: cd.name,
|
|
5976
|
+
headerName: cd.name,
|
|
5977
|
+
flex: 1,
|
|
5978
|
+
minWidth: 150,
|
|
5979
|
+
type: 'string'
|
|
5980
|
+
};
|
|
5981
|
+
}
|
|
5982
|
+
if (item.kind === 'metric') {
|
|
5983
|
+
const metric = item.data;
|
|
5984
|
+
const metricDef = metric.metric;
|
|
5985
|
+
let fieldName;
|
|
5986
|
+
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5987
|
+
if (dotCount >= 2) {
|
|
5988
|
+
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5989
|
+
// Normalise dots to underscores to match the row key normalisation below.
|
|
5990
|
+
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5991
|
+
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5992
|
+
} else if (metric.relations && metric.relations.length > 0) {
|
|
5993
|
+
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5994
|
+
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5995
|
+
const parts = metric.fullPath.split('.');
|
|
5996
|
+
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5997
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
5998
|
+
|
|
5999
|
+
const pathParts = pathWithoutField.split('_');
|
|
6000
|
+
pathParts.shift(); // remove base provider alias
|
|
6001
|
+
const pathWithoutBase = pathParts.join('_');
|
|
6002
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6003
|
+
} else {
|
|
6004
|
+
// 2-part path from the base provider: API returns just the metric name.
|
|
6005
|
+
// Example: "mc.record_count" -> "record_count"
|
|
6006
|
+
fieldName = metric.metricName;
|
|
6007
|
+
}
|
|
6008
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName, parameters);
|
|
6009
|
+
return {
|
|
6010
|
+
field: fieldName,
|
|
6011
|
+
headerName: headerName,
|
|
6012
|
+
flex: 1,
|
|
6013
|
+
minWidth: 150,
|
|
6014
|
+
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
6015
|
+
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6016
|
+
sx: {
|
|
6017
|
+
display: 'flex',
|
|
6018
|
+
alignItems: 'center',
|
|
6019
|
+
gap: 0.5
|
|
6020
|
+
}
|
|
6021
|
+
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
6022
|
+
source: metricDef?.source
|
|
6023
|
+
}), /*#__PURE__*/React__namespace.default.createElement("span", null, headerName)),
|
|
6024
|
+
valueFormatter: value => formatValue(value, metricDef)
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
// Dimension
|
|
6029
|
+
const dim = item.data;
|
|
6030
|
+
let fieldName;
|
|
6031
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
6032
|
+
// From nested provider: drop the base provider alias
|
|
6033
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
6034
|
+
const parts = dim.fullPath.split('.');
|
|
6035
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
6036
|
+
const field = parts[1]; // e.g., "currency"
|
|
6037
|
+
|
|
6038
|
+
const pathParts = pathWithoutField.split('_');
|
|
6039
|
+
pathParts.shift(); // Remove base provider alias
|
|
6040
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
6041
|
+
|
|
6042
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6043
|
+
} else {
|
|
6044
|
+
// From base provider: keep the full path with underscore
|
|
6045
|
+
// Example: ba.created_at -> ba_created_at
|
|
6046
|
+
fieldName = dim.fullPath.replace('.', '_');
|
|
6047
|
+
}
|
|
6048
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName, parameters);
|
|
6049
|
+
const dimDef = dim.dimension;
|
|
6050
|
+
return {
|
|
6051
|
+
field: fieldName,
|
|
6052
|
+
headerName: headerName,
|
|
6053
|
+
flex: 1,
|
|
6054
|
+
minWidth: 150,
|
|
6055
|
+
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
6056
|
+
valueFormatter: value => formatValue(value, dimDef)
|
|
6057
|
+
};
|
|
6058
|
+
};
|
|
5783
6059
|
const ReportDataGrid = ({
|
|
5784
6060
|
reportData,
|
|
5785
|
-
|
|
5786
|
-
metrics,
|
|
5787
|
-
computedDimensions = [],
|
|
6061
|
+
columnItems = [],
|
|
5788
6062
|
loading,
|
|
5789
6063
|
onPageChange,
|
|
5790
6064
|
totalRows = 0,
|
|
@@ -5800,124 +6074,10 @@ const ReportDataGrid = ({
|
|
|
5800
6074
|
pageSize: 50
|
|
5801
6075
|
});
|
|
5802
6076
|
|
|
5803
|
-
// Generate columns dynamically
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
// Add dimension columns
|
|
5808
|
-
// Field naming logic:
|
|
5809
|
-
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5810
|
-
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5811
|
-
dimensions.forEach(dim => {
|
|
5812
|
-
let fieldName;
|
|
5813
|
-
let headerName;
|
|
5814
|
-
|
|
5815
|
-
// Check if there are relations (nested providers)
|
|
5816
|
-
if (dim.relations && dim.relations.length > 0) {
|
|
5817
|
-
// From nested provider: drop the base provider alias
|
|
5818
|
-
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5819
|
-
const parts = dim.fullPath.split('.');
|
|
5820
|
-
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5821
|
-
const field = parts[1]; // e.g., "currency"
|
|
5822
|
-
|
|
5823
|
-
// Remove the base provider alias (first part before first underscore)
|
|
5824
|
-
const pathParts = pathWithoutField.split('_');
|
|
5825
|
-
pathParts.shift(); // Remove base provider alias
|
|
5826
|
-
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5827
|
-
|
|
5828
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5829
|
-
} else {
|
|
5830
|
-
// From base provider: keep the full path with underscore
|
|
5831
|
-
// Example: ba.created_at -> ba_created_at
|
|
5832
|
-
fieldName = dim.fullPath.replace('.', '_');
|
|
5833
|
-
}
|
|
5834
|
-
|
|
5835
|
-
// Check for title override, otherwise use the friendly dimension title from provider
|
|
5836
|
-
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
5837
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5838
|
-
const dimDef = dim.dimension;
|
|
5839
|
-
cols.push({
|
|
5840
|
-
field: fieldName,
|
|
5841
|
-
headerName: headerName,
|
|
5842
|
-
flex: 1,
|
|
5843
|
-
minWidth: 150,
|
|
5844
|
-
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5845
|
-
valueFormatter: value => formatValue(value, dimDef)
|
|
5846
|
-
});
|
|
5847
|
-
});
|
|
5848
|
-
|
|
5849
|
-
// Add computed dimension columns. These have no provider/relations chain,
|
|
5850
|
-
// so the API returns each row keyed by the computed dimension's own `name`.
|
|
5851
|
-
computedDimensions.forEach(cd => {
|
|
5852
|
-
cols.push({
|
|
5853
|
-
field: cd.name,
|
|
5854
|
-
headerName: cd.name,
|
|
5855
|
-
flex: 1,
|
|
5856
|
-
minWidth: 150,
|
|
5857
|
-
type: 'string'
|
|
5858
|
-
});
|
|
5859
|
-
});
|
|
5860
|
-
|
|
5861
|
-
// Add metric columns
|
|
5862
|
-
// The API returns metric keys in two different forms depending on the path depth:
|
|
5863
|
-
//
|
|
5864
|
-
// • 2-part path "mc.record_count" → API key: "record_count"
|
|
5865
|
-
// • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
|
|
5866
|
-
// • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
|
|
5867
|
-
//
|
|
5868
|
-
// For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
|
|
5869
|
-
// in field names as nested-object accessors, so the rows useMemo normalises
|
|
5870
|
-
// those keys (dots → underscores). The column field must match that normalised key.
|
|
5871
|
-
metrics.forEach(metric => {
|
|
5872
|
-
const metricDef = metric.metric;
|
|
5873
|
-
let fieldName;
|
|
5874
|
-
let headerName;
|
|
5875
|
-
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5876
|
-
if (dotCount >= 2) {
|
|
5877
|
-
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5878
|
-
// Normalise dots to underscores to match the row key normalisation below.
|
|
5879
|
-
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5880
|
-
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5881
|
-
} else if (metric.relations && metric.relations.length > 0) {
|
|
5882
|
-
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5883
|
-
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5884
|
-
const parts = metric.fullPath.split('.');
|
|
5885
|
-
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5886
|
-
const field = parts[1]; // e.g., "total_amount"
|
|
5887
|
-
|
|
5888
|
-
const pathParts = pathWithoutField.split('_');
|
|
5889
|
-
pathParts.shift(); // remove base provider alias
|
|
5890
|
-
const pathWithoutBase = pathParts.join('_');
|
|
5891
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5892
|
-
} else {
|
|
5893
|
-
// 2-part path from the base provider: API returns just the metric name.
|
|
5894
|
-
// Example: "mc.record_count" -> "record_count"
|
|
5895
|
-
fieldName = metric.metricName;
|
|
5896
|
-
}
|
|
5897
|
-
|
|
5898
|
-
// Check for title override, otherwise use the friendly metric title from provider
|
|
5899
|
-
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
5900
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5901
|
-
cols.push({
|
|
5902
|
-
field: fieldName,
|
|
5903
|
-
headerName: headerName,
|
|
5904
|
-
flex: 1,
|
|
5905
|
-
minWidth: 150,
|
|
5906
|
-
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5907
|
-
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5908
|
-
sx: {
|
|
5909
|
-
display: 'flex',
|
|
5910
|
-
alignItems: 'center',
|
|
5911
|
-
gap: 0.5
|
|
5912
|
-
}
|
|
5913
|
-
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
5914
|
-
source: metricDef?.source
|
|
5915
|
-
}), /*#__PURE__*/React__namespace.default.createElement("span", null, headerName)),
|
|
5916
|
-
valueFormatter: value => formatValue(value, metricDef)
|
|
5917
|
-
});
|
|
5918
|
-
});
|
|
5919
|
-
return cols;
|
|
5920
|
-
}, [dimensions, metrics, computedDimensions, titleOverrides, parameters]);
|
|
6077
|
+
// Generate columns dynamically, in the final resolved column order
|
|
6078
|
+
// (custom Column Order tab arrangement when defined, otherwise
|
|
6079
|
+
// dimensions/computed then metrics).
|
|
6080
|
+
const columns = React__namespace.default.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
|
|
5921
6081
|
|
|
5922
6082
|
// Transform report data to rows with unique IDs.
|
|
5923
6083
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -6024,7 +6184,15 @@ const ReportBuilder = ({
|
|
|
6024
6184
|
dimensions: [],
|
|
6025
6185
|
metrics: [],
|
|
6026
6186
|
filters: {},
|
|
6027
|
-
computedDimensions: []
|
|
6187
|
+
computedDimensions: [],
|
|
6188
|
+
// Unified display/execution order across dimensions + computedDimensions.
|
|
6189
|
+
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
6190
|
+
dimensionOrder: [],
|
|
6191
|
+
// Optional unified display order across dimensions + computedDimensions +
|
|
6192
|
+
// metrics, set on the Column Order tab. Empty means "not defined" — the
|
|
6193
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6194
|
+
// Entries: { type: 'dimension' | 'computed' | 'metric', key }
|
|
6195
|
+
columnOrder: []
|
|
6028
6196
|
});
|
|
6029
6197
|
const [titleOverrides, setTitleOverrides] = React.useState({
|
|
6030
6198
|
dimensions: {},
|
|
@@ -6070,6 +6238,16 @@ const ReportBuilder = ({
|
|
|
6070
6238
|
}
|
|
6071
6239
|
}, [autoRun, reportLoaded]);
|
|
6072
6240
|
|
|
6241
|
+
// Re-run the currently displayed report when base_currency changes in context
|
|
6242
|
+
const currentBaseCurrency = reportingContext?.parameters?.base_currency;
|
|
6243
|
+
const prevBaseCurrencyRef = React.useRef(currentBaseCurrency);
|
|
6244
|
+
React.useEffect(() => {
|
|
6245
|
+
if (reportData && prevBaseCurrencyRef.current !== undefined && prevBaseCurrencyRef.current !== currentBaseCurrency) {
|
|
6246
|
+
runReportWithPagination(currentPage, pageSize);
|
|
6247
|
+
}
|
|
6248
|
+
prevBaseCurrencyRef.current = currentBaseCurrency;
|
|
6249
|
+
}, [currentBaseCurrency]);
|
|
6250
|
+
|
|
6073
6251
|
// Utility function to reconstruct dimension object from fullPath
|
|
6074
6252
|
const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
|
|
6075
6253
|
try {
|
|
@@ -6199,6 +6377,92 @@ const ReportBuilder = ({
|
|
|
6199
6377
|
return null;
|
|
6200
6378
|
}
|
|
6201
6379
|
};
|
|
6380
|
+
|
|
6381
|
+
// Build the unified display/execution order for dimensions + computed
|
|
6382
|
+
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6383
|
+
// to "dimensions first, then computed dimensions" for report definitions
|
|
6384
|
+
// saved before this field existed.
|
|
6385
|
+
const buildDimensionOrder = (rawOrderedDimensions, dimensions, computedDimensions) => {
|
|
6386
|
+
const dimensionOrder = [];
|
|
6387
|
+
const seenDimensionKeys = new Set();
|
|
6388
|
+
const seenComputedKeys = new Set();
|
|
6389
|
+
if (Array.isArray(rawOrderedDimensions)) {
|
|
6390
|
+
rawOrderedDimensions.forEach(entry => {
|
|
6391
|
+
if (entry.type === "dimension" && dimensions.some(d => d.fullPath === entry.ref)) {
|
|
6392
|
+
dimensionOrder.push({
|
|
6393
|
+
type: "dimension",
|
|
6394
|
+
key: entry.ref
|
|
6395
|
+
});
|
|
6396
|
+
seenDimensionKeys.add(entry.ref);
|
|
6397
|
+
} else if (entry.type === "computed" && computedDimensions.some(cd => cd.name === entry.ref)) {
|
|
6398
|
+
dimensionOrder.push({
|
|
6399
|
+
type: "computed",
|
|
6400
|
+
key: entry.ref
|
|
6401
|
+
});
|
|
6402
|
+
seenComputedKeys.add(entry.ref);
|
|
6403
|
+
}
|
|
6404
|
+
});
|
|
6405
|
+
}
|
|
6406
|
+
|
|
6407
|
+
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6408
|
+
dimensions.forEach(d => {
|
|
6409
|
+
if (!seenDimensionKeys.has(d.fullPath)) {
|
|
6410
|
+
dimensionOrder.push({
|
|
6411
|
+
type: "dimension",
|
|
6412
|
+
key: d.fullPath
|
|
6413
|
+
});
|
|
6414
|
+
}
|
|
6415
|
+
});
|
|
6416
|
+
computedDimensions.forEach(cd => {
|
|
6417
|
+
if (!seenComputedKeys.has(cd.name)) {
|
|
6418
|
+
dimensionOrder.push({
|
|
6419
|
+
type: "computed",
|
|
6420
|
+
key: cd.name
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
});
|
|
6424
|
+
return dimensionOrder;
|
|
6425
|
+
};
|
|
6426
|
+
|
|
6427
|
+
// Build the optional unified column order (dimensions + computed
|
|
6428
|
+
// dimensions + metrics) from the persisted `ordered_columns` field.
|
|
6429
|
+
// Unlike buildDimensionOrder, entries that no longer resolve are simply
|
|
6430
|
+
// dropped rather than backfilled — an empty/partial result just means "no
|
|
6431
|
+
// custom order defined (yet)", which resolveColumnOrder treats as
|
|
6432
|
+
// "fall back to the default order".
|
|
6433
|
+
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6434
|
+
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6435
|
+
const dimensionPaths = new Set(dimensions.map(d => d.fullPath));
|
|
6436
|
+
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6437
|
+
const metricPaths = new Set(metrics.map(m => m.fullPath));
|
|
6438
|
+
const columnOrder = [];
|
|
6439
|
+
rawOrderedColumns.forEach(entry => {
|
|
6440
|
+
if (entry.type === "dimension" && dimensionPaths.has(entry.ref)) {
|
|
6441
|
+
columnOrder.push({
|
|
6442
|
+
type: "dimension",
|
|
6443
|
+
key: entry.ref
|
|
6444
|
+
});
|
|
6445
|
+
} else if (entry.type === "computed" && computedNames.has(entry.ref)) {
|
|
6446
|
+
columnOrder.push({
|
|
6447
|
+
type: "computed",
|
|
6448
|
+
key: entry.ref
|
|
6449
|
+
});
|
|
6450
|
+
} else if (entry.type === "metric" && metricPaths.has(entry.ref)) {
|
|
6451
|
+
columnOrder.push({
|
|
6452
|
+
type: "metric",
|
|
6453
|
+
key: entry.ref
|
|
6454
|
+
});
|
|
6455
|
+
}
|
|
6456
|
+
});
|
|
6457
|
+
return columnOrder;
|
|
6458
|
+
};
|
|
6459
|
+
|
|
6460
|
+
// Auto-append a newly added dimension/computed dimension/metric to the end
|
|
6461
|
+
// of columnOrder — but only once a custom order actually exists. While
|
|
6462
|
+
// columnOrder is empty (no custom order defined) it's left untouched, so
|
|
6463
|
+
// the results grid keeps falling back to the default dimensions-then-
|
|
6464
|
+
// metrics order until the user visits the Column Order tab.
|
|
6465
|
+
const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
|
|
6202
6466
|
const loadReportDefinition = async id => {
|
|
6203
6467
|
try {
|
|
6204
6468
|
console.log("Loading report definition:", id);
|
|
@@ -6294,13 +6558,17 @@ const ReportBuilder = ({
|
|
|
6294
6558
|
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6295
6559
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6296
6560
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6561
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6562
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6297
6563
|
|
|
6298
6564
|
// Set the report state
|
|
6299
6565
|
setReport({
|
|
6300
6566
|
dimensions: reconstructedDimensions,
|
|
6301
6567
|
metrics: reconstructedMetrics,
|
|
6302
6568
|
filters: loadedFilters,
|
|
6303
|
-
computedDimensions
|
|
6569
|
+
computedDimensions,
|
|
6570
|
+
dimensionOrder,
|
|
6571
|
+
columnOrder
|
|
6304
6572
|
});
|
|
6305
6573
|
|
|
6306
6574
|
// Set title overrides
|
|
@@ -6409,13 +6677,17 @@ const ReportBuilder = ({
|
|
|
6409
6677
|
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6410
6678
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6411
6679
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6680
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6681
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6412
6682
|
|
|
6413
6683
|
// Set the report state
|
|
6414
6684
|
setReport({
|
|
6415
6685
|
dimensions: reconstructedDimensions,
|
|
6416
6686
|
metrics: reconstructedMetrics,
|
|
6417
6687
|
filters: loadedFilters,
|
|
6418
|
-
computedDimensions
|
|
6688
|
+
computedDimensions,
|
|
6689
|
+
dimensionOrder,
|
|
6690
|
+
columnOrder
|
|
6419
6691
|
});
|
|
6420
6692
|
|
|
6421
6693
|
// Set title overrides
|
|
@@ -6448,7 +6720,9 @@ const ReportBuilder = ({
|
|
|
6448
6720
|
dimensions: [],
|
|
6449
6721
|
metrics: [],
|
|
6450
6722
|
filters: {},
|
|
6451
|
-
computedDimensions: []
|
|
6723
|
+
computedDimensions: [],
|
|
6724
|
+
dimensionOrder: [],
|
|
6725
|
+
columnOrder: []
|
|
6452
6726
|
});
|
|
6453
6727
|
// Reset title overrides
|
|
6454
6728
|
setTitleOverrides({
|
|
@@ -6525,58 +6799,93 @@ const ReportBuilder = ({
|
|
|
6525
6799
|
setReport(prev => {
|
|
6526
6800
|
const newReport = {
|
|
6527
6801
|
...prev,
|
|
6528
|
-
dimensions: [...prev.dimensions, dimensionData]
|
|
6802
|
+
dimensions: [...prev.dimensions, dimensionData],
|
|
6803
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6804
|
+
type: "dimension",
|
|
6805
|
+
key: dimensionData.fullPath
|
|
6806
|
+
}],
|
|
6807
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6808
|
+
type: "dimension",
|
|
6809
|
+
key: dimensionData.fullPath
|
|
6810
|
+
})
|
|
6529
6811
|
};
|
|
6530
6812
|
console.log("Dimension saved:", dimensionData);
|
|
6531
6813
|
console.log("Complete report:", newReport);
|
|
6532
6814
|
return newReport;
|
|
6533
6815
|
});
|
|
6534
6816
|
};
|
|
6535
|
-
const handleRemoveDimension =
|
|
6817
|
+
const handleRemoveDimension = fullPath => {
|
|
6536
6818
|
setReport(prev => ({
|
|
6537
6819
|
...prev,
|
|
6538
|
-
dimensions: prev.dimensions.filter(
|
|
6820
|
+
dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
|
|
6821
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath)),
|
|
6822
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6539
6823
|
}));
|
|
6540
6824
|
};
|
|
6541
|
-
const
|
|
6825
|
+
const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
|
|
6542
6826
|
setReport(prev => ({
|
|
6543
6827
|
...prev,
|
|
6544
|
-
dimensions:
|
|
6828
|
+
dimensions: prev.dimensions.map(d => d.fullPath === fullPath ? {
|
|
6829
|
+
...d,
|
|
6830
|
+
sortOrder
|
|
6831
|
+
} : d)
|
|
6545
6832
|
}));
|
|
6546
6833
|
};
|
|
6547
|
-
|
|
6834
|
+
|
|
6835
|
+
// Reorders the single unified dimensions + computed-dimensions list;
|
|
6836
|
+
// `dimensions`/`computedDimensions` arrays themselves are unordered from
|
|
6837
|
+
// this perspective, so only `dimensionOrder` needs to change.
|
|
6838
|
+
const handleReorderDimensionItems = newDimensionOrder => {
|
|
6548
6839
|
setReport(prev => ({
|
|
6549
6840
|
...prev,
|
|
6550
|
-
|
|
6841
|
+
dimensionOrder: newDimensionOrder
|
|
6551
6842
|
}));
|
|
6552
6843
|
};
|
|
6553
|
-
const
|
|
6554
|
-
setReport(prev => {
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
}
|
|
6561
|
-
|
|
6844
|
+
const handleSaveComputedDimension = computedDimensionData => {
|
|
6845
|
+
setReport(prev => ({
|
|
6846
|
+
...prev,
|
|
6847
|
+
computedDimensions: [...prev.computedDimensions, computedDimensionData],
|
|
6848
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6849
|
+
type: "computed",
|
|
6850
|
+
key: computedDimensionData.name
|
|
6851
|
+
}],
|
|
6852
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6853
|
+
type: "computed",
|
|
6854
|
+
key: computedDimensionData.name
|
|
6855
|
+
})
|
|
6856
|
+
}));
|
|
6562
6857
|
};
|
|
6563
|
-
const
|
|
6858
|
+
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
6564
6859
|
setReport(prev => ({
|
|
6565
6860
|
...prev,
|
|
6566
|
-
computedDimensions: prev.computedDimensions.
|
|
6861
|
+
computedDimensions: prev.computedDimensions.map(cd => cd.name === name ? computedDimensionData : cd),
|
|
6862
|
+
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6863
|
+
...entry,
|
|
6864
|
+
key: computedDimensionData.name
|
|
6865
|
+
} : entry),
|
|
6866
|
+
columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6867
|
+
...entry,
|
|
6868
|
+
key: computedDimensionData.name
|
|
6869
|
+
} : entry)
|
|
6567
6870
|
}));
|
|
6568
6871
|
};
|
|
6569
|
-
const
|
|
6872
|
+
const handleRemoveComputedDimension = name => {
|
|
6570
6873
|
setReport(prev => ({
|
|
6571
6874
|
...prev,
|
|
6572
|
-
computedDimensions:
|
|
6875
|
+
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6876
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
|
|
6877
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6573
6878
|
}));
|
|
6574
6879
|
};
|
|
6575
6880
|
const handleSaveMetric = metricData => {
|
|
6576
6881
|
setReport(prev => {
|
|
6577
6882
|
const newReport = {
|
|
6578
6883
|
...prev,
|
|
6579
|
-
metrics: [...prev.metrics, metricData]
|
|
6884
|
+
metrics: [...prev.metrics, metricData],
|
|
6885
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6886
|
+
type: "metric",
|
|
6887
|
+
key: metricData.fullPath
|
|
6888
|
+
})
|
|
6580
6889
|
};
|
|
6581
6890
|
console.log("Metric saved:", metricData);
|
|
6582
6891
|
console.log("Complete report:", newReport);
|
|
@@ -6584,15 +6893,29 @@ const ReportBuilder = ({
|
|
|
6584
6893
|
});
|
|
6585
6894
|
};
|
|
6586
6895
|
const handleRemoveMetric = index => {
|
|
6896
|
+
setReport(prev => {
|
|
6897
|
+
const fullPath = prev.metrics[index]?.fullPath;
|
|
6898
|
+
return {
|
|
6899
|
+
...prev,
|
|
6900
|
+
metrics: prev.metrics.filter((_, i) => i !== index),
|
|
6901
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
6902
|
+
};
|
|
6903
|
+
});
|
|
6904
|
+
};
|
|
6905
|
+
const handleReorderMetrics = newOrder => {
|
|
6587
6906
|
setReport(prev => ({
|
|
6588
6907
|
...prev,
|
|
6589
|
-
metrics:
|
|
6908
|
+
metrics: newOrder
|
|
6590
6909
|
}));
|
|
6591
6910
|
};
|
|
6592
|
-
|
|
6911
|
+
|
|
6912
|
+
// Sets the unified column order shown on the Column Order tab. An empty
|
|
6913
|
+
// array (e.g. via "Reset to Default") clears the customization, and the
|
|
6914
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6915
|
+
const handleReorderColumnItems = newColumnOrder => {
|
|
6593
6916
|
setReport(prev => ({
|
|
6594
6917
|
...prev,
|
|
6595
|
-
|
|
6918
|
+
columnOrder: newColumnOrder
|
|
6596
6919
|
}));
|
|
6597
6920
|
};
|
|
6598
6921
|
const handleSaveFilter = (fullPath, filterData) => {
|
|
@@ -6625,10 +6948,20 @@ const ReportBuilder = ({
|
|
|
6625
6948
|
});
|
|
6626
6949
|
};
|
|
6627
6950
|
|
|
6628
|
-
//
|
|
6629
|
-
|
|
6951
|
+
// Builds the query object shared between running (paginated) and saving
|
|
6952
|
+
// (unpaginated) a report. Dimension and computed-dimension order both
|
|
6953
|
+
// derive from the single unified `report.dimensionOrder` list, so the
|
|
6954
|
+
// saved `dimensions`/`computed_dimensions` arrays, `order_by`, and the
|
|
6955
|
+
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6956
|
+
// order the user arranged on screen.
|
|
6957
|
+
const buildQueryObject = () => {
|
|
6958
|
+
const dimensionByPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
6959
|
+
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6960
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionByPath[entry.key]).filter(Boolean);
|
|
6961
|
+
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6962
|
+
|
|
6630
6963
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6631
|
-
const orderBy =
|
|
6964
|
+
const orderBy = orderedDimensionsOnly.map(dim => {
|
|
6632
6965
|
const orderItem = {
|
|
6633
6966
|
name: dim.fullPath
|
|
6634
6967
|
};
|
|
@@ -6684,21 +7017,40 @@ const ReportBuilder = ({
|
|
|
6684
7017
|
titles.filters = titleOverrides.filters;
|
|
6685
7018
|
}
|
|
6686
7019
|
const queryObj = {
|
|
6687
|
-
dimensions:
|
|
7020
|
+
dimensions: orderedDimensionsOnly.map(dim => dim.fullPath),
|
|
6688
7021
|
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6689
|
-
order_by: orderBy
|
|
6690
|
-
limit: size,
|
|
6691
|
-
offset: page * size
|
|
7022
|
+
order_by: orderBy
|
|
6692
7023
|
};
|
|
6693
7024
|
|
|
6694
7025
|
// Only add computed_dimensions if there are any
|
|
6695
|
-
if (
|
|
6696
|
-
queryObj.computed_dimensions =
|
|
7026
|
+
if (orderedComputedOnly.length > 0) {
|
|
7027
|
+
queryObj.computed_dimensions = orderedComputedOnly.map(cd => ({
|
|
6697
7028
|
name: cd.name,
|
|
6698
7029
|
value: cd.value
|
|
6699
7030
|
}));
|
|
6700
7031
|
}
|
|
6701
7032
|
|
|
7033
|
+
// Persist the unified dimension + computed-dimension order so it can be
|
|
7034
|
+
// reconstructed on reload and so executed reports render columns in the
|
|
7035
|
+
// same order they were arranged in the builder.
|
|
7036
|
+
if (report.dimensionOrder.length > 0) {
|
|
7037
|
+
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
7038
|
+
type: entry.type,
|
|
7039
|
+
ref: entry.key
|
|
7040
|
+
}));
|
|
7041
|
+
}
|
|
7042
|
+
|
|
7043
|
+
// Persist the optional unified column order (Column Order tab) so the
|
|
7044
|
+
// results grid can reproduce it on reload. Absent when no custom order
|
|
7045
|
+
// has been defined, so old/untouched reports keep their current
|
|
7046
|
+
// dimensions-then-metrics column order unchanged.
|
|
7047
|
+
if (report.columnOrder.length > 0) {
|
|
7048
|
+
queryObj.ordered_columns = report.columnOrder.map(entry => ({
|
|
7049
|
+
type: entry.type,
|
|
7050
|
+
ref: entry.key
|
|
7051
|
+
}));
|
|
7052
|
+
}
|
|
7053
|
+
|
|
6702
7054
|
// Only add titles if there are any overrides
|
|
6703
7055
|
if (Object.keys(titles).length > 0) {
|
|
6704
7056
|
queryObj.titles = titles;
|
|
@@ -6708,6 +7060,14 @@ const ReportBuilder = ({
|
|
|
6708
7060
|
if (filter) {
|
|
6709
7061
|
queryObj.filter = filter;
|
|
6710
7062
|
}
|
|
7063
|
+
return queryObj;
|
|
7064
|
+
};
|
|
7065
|
+
|
|
7066
|
+
// Convert internal report structure to API format
|
|
7067
|
+
const convertReportToApiFormat = (page = 0, size = 50) => {
|
|
7068
|
+
const queryObj = buildQueryObject();
|
|
7069
|
+
queryObj.limit = size;
|
|
7070
|
+
queryObj.offset = page * size;
|
|
6711
7071
|
|
|
6712
7072
|
// Get parameters from context if available, otherwise use default
|
|
6713
7073
|
const parameters = reportingContext?.parameters || {
|
|
@@ -6750,8 +7110,8 @@ const ReportBuilder = ({
|
|
|
6750
7110
|
const handleRunReport = async () => {
|
|
6751
7111
|
setCurrentPage(0);
|
|
6752
7112
|
await runReportWithPagination(0, pageSize);
|
|
6753
|
-
// Switch to Results tab after running report (
|
|
6754
|
-
setActiveTab(
|
|
7113
|
+
// Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
|
|
7114
|
+
setActiveTab(4);
|
|
6755
7115
|
};
|
|
6756
7116
|
const handleDownloadReport = async () => {
|
|
6757
7117
|
try {
|
|
@@ -6801,77 +7161,7 @@ const ReportBuilder = ({
|
|
|
6801
7161
|
}
|
|
6802
7162
|
try {
|
|
6803
7163
|
setSaving(true);
|
|
6804
|
-
|
|
6805
|
-
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6806
|
-
const orderBy = report.dimensions.map(dim => {
|
|
6807
|
-
const orderItem = {
|
|
6808
|
-
name: dim.fullPath
|
|
6809
|
-
};
|
|
6810
|
-
if (dim.sortOrder === "desc") {
|
|
6811
|
-
orderItem.desc = true;
|
|
6812
|
-
}
|
|
6813
|
-
// For 'asc' or null, just include { name: fullPath } without desc property
|
|
6814
|
-
return orderItem;
|
|
6815
|
-
});
|
|
6816
|
-
|
|
6817
|
-
// Build titles object - only include overridden titles
|
|
6818
|
-
const titles = {};
|
|
6819
|
-
if (Object.keys(titleOverrides.dimensions).length > 0) {
|
|
6820
|
-
titles.dimensions = titleOverrides.dimensions;
|
|
6821
|
-
}
|
|
6822
|
-
if (Object.keys(titleOverrides.metrics).length > 0) {
|
|
6823
|
-
titles.metrics = titleOverrides.metrics;
|
|
6824
|
-
}
|
|
6825
|
-
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
6826
|
-
titles.filters = titleOverrides.filters;
|
|
6827
|
-
}
|
|
6828
|
-
const queryObj = {
|
|
6829
|
-
dimensions: report.dimensions.map(dim => dim.fullPath),
|
|
6830
|
-
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6831
|
-
order_by: orderBy
|
|
6832
|
-
};
|
|
6833
|
-
|
|
6834
|
-
// Only add computed_dimensions if there are any
|
|
6835
|
-
if (report.computedDimensions && report.computedDimensions.length > 0) {
|
|
6836
|
-
queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
|
|
6837
|
-
name: cd.name,
|
|
6838
|
-
value: cd.value
|
|
6839
|
-
}));
|
|
6840
|
-
}
|
|
6841
|
-
|
|
6842
|
-
// Only add titles if there are any overrides
|
|
6843
|
-
if (Object.keys(titles).length > 0) {
|
|
6844
|
-
queryObj.titles = titles;
|
|
6845
|
-
}
|
|
6846
|
-
|
|
6847
|
-
// Add filter if there are any - NEW API format with top-level 'and' operator
|
|
6848
|
-
// NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
|
|
6849
|
-
if (Object.keys(report.filters).length > 0) {
|
|
6850
|
-
const conditions = [];
|
|
6851
|
-
Object.entries(report.filters).forEach(([fullPath, filterData]) => {
|
|
6852
|
-
if (filterData.values) {
|
|
6853
|
-
// Check if values is an object (date range) or array (regular filter)
|
|
6854
|
-
if (Array.isArray(filterData.values)) {
|
|
6855
|
-
// Regular filter - only add if array has values
|
|
6856
|
-
if (filterData.values.length > 0) {
|
|
6857
|
-
conditions.push({
|
|
6858
|
-
[fullPath]: filterData.values
|
|
6859
|
-
});
|
|
6860
|
-
}
|
|
6861
|
-
} else if (typeof filterData.values === "object") {
|
|
6862
|
-
// Date range filter - add the object as-is
|
|
6863
|
-
conditions.push({
|
|
6864
|
-
[fullPath]: filterData.values
|
|
6865
|
-
});
|
|
6866
|
-
}
|
|
6867
|
-
}
|
|
6868
|
-
});
|
|
6869
|
-
if (conditions.length > 0) {
|
|
6870
|
-
queryObj.filter = {
|
|
6871
|
-
and: conditions
|
|
6872
|
-
};
|
|
6873
|
-
}
|
|
6874
|
-
}
|
|
7164
|
+
const queryObj = buildQueryObject();
|
|
6875
7165
|
|
|
6876
7166
|
// Build the doc object
|
|
6877
7167
|
const docObj = {
|
|
@@ -6916,6 +7206,23 @@ const ReportBuilder = ({
|
|
|
6916
7206
|
}
|
|
6917
7207
|
};
|
|
6918
7208
|
const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0);
|
|
7209
|
+
|
|
7210
|
+
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
7211
|
+
// objects so the results grid can render columns in that same order.
|
|
7212
|
+
const dimensionByFullPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
7213
|
+
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
7214
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
7215
|
+
kind: "dimension",
|
|
7216
|
+
data: dimensionByFullPath[entry.key]
|
|
7217
|
+
} : {
|
|
7218
|
+
kind: "computed",
|
|
7219
|
+
data: computedDimensionByName[entry.key]
|
|
7220
|
+
}).filter(item => item.data);
|
|
7221
|
+
|
|
7222
|
+
// Final column list for the results grid: the explicit Column Order tab
|
|
7223
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7224
|
+
// order) followed by metrics (in their own order) — today's behavior.
|
|
7225
|
+
const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
6919
7226
|
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6920
7227
|
sx: {
|
|
6921
7228
|
p: 3,
|
|
@@ -7170,9 +7477,36 @@ const ReportBuilder = ({
|
|
|
7170
7477
|
}
|
|
7171
7478
|
}
|
|
7172
7479
|
}), /*#__PURE__*/React__namespace.default.createElement(material.Tab, {
|
|
7173
|
-
label:
|
|
7480
|
+
label: /*#__PURE__*/React__namespace.default.createElement(material.Badge, {
|
|
7481
|
+
variant: "dot",
|
|
7482
|
+
invisible: report.columnOrder.length === 0,
|
|
7483
|
+
sx: {
|
|
7484
|
+
"& .MuiBadge-dot": {
|
|
7485
|
+
backgroundColor: "rgb(70, 134, 128)"
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
}, /*#__PURE__*/React__namespace.default.createElement("span", {
|
|
7489
|
+
style: {
|
|
7490
|
+
marginRight: report.columnOrder.length > 0 ? "8px" : "0"
|
|
7491
|
+
}
|
|
7492
|
+
}, "Column Order")),
|
|
7174
7493
|
id: "report-tab-3",
|
|
7175
7494
|
"aria-controls": "report-tabpanel-3",
|
|
7495
|
+
sx: {
|
|
7496
|
+
height: "41px",
|
|
7497
|
+
fontFamily: "system-ui",
|
|
7498
|
+
borderRadius: "0.5rem",
|
|
7499
|
+
boxShadow: "none",
|
|
7500
|
+
textTransform: "none",
|
|
7501
|
+
color: "rgb(37, 37, 37)",
|
|
7502
|
+
"&.Mui-selected": {
|
|
7503
|
+
color: "rgb(70, 134, 128)"
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Tab, {
|
|
7507
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
7508
|
+
id: "report-tab-4",
|
|
7509
|
+
"aria-controls": "report-tabpanel-4",
|
|
7176
7510
|
disabled: !reportData,
|
|
7177
7511
|
sx: {
|
|
7178
7512
|
height: "41px",
|
|
@@ -7194,7 +7528,7 @@ const ReportBuilder = ({
|
|
|
7194
7528
|
savedDimensions: report.dimensions,
|
|
7195
7529
|
onSaveDimension: handleSaveDimension,
|
|
7196
7530
|
onRemoveDimension: handleRemoveDimension,
|
|
7197
|
-
|
|
7531
|
+
onUpdateDimensionSortOrder: handleUpdateDimensionSortOrder,
|
|
7198
7532
|
titleOverrides: titleOverrides.dimensions,
|
|
7199
7533
|
onUpdateTitle: handleUpdateDimensionTitle,
|
|
7200
7534
|
onResetTitle: handleResetDimensionTitle,
|
|
@@ -7204,7 +7538,8 @@ const ReportBuilder = ({
|
|
|
7204
7538
|
onSaveComputedDimension: handleSaveComputedDimension,
|
|
7205
7539
|
onUpdateComputedDimension: handleUpdateComputedDimension,
|
|
7206
7540
|
onRemoveComputedDimension: handleRemoveComputedDimension,
|
|
7207
|
-
|
|
7541
|
+
dimensionOrder: report.dimensionOrder,
|
|
7542
|
+
onReorderDimensionItems: handleReorderDimensionItems
|
|
7208
7543
|
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7209
7544
|
value: activeTab,
|
|
7210
7545
|
index: 1
|
|
@@ -7238,11 +7573,18 @@ const ReportBuilder = ({
|
|
|
7238
7573
|
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7239
7574
|
value: activeTab,
|
|
7240
7575
|
index: 3
|
|
7576
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ColumnOrder, {
|
|
7577
|
+
orderedDimensionItems: orderedDimensionItems,
|
|
7578
|
+
metrics: report.metrics,
|
|
7579
|
+
columnOrder: report.columnOrder,
|
|
7580
|
+
onReorderColumnItems: handleReorderColumnItems,
|
|
7581
|
+
titleOverrides: titleOverrides
|
|
7582
|
+
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7583
|
+
value: activeTab,
|
|
7584
|
+
index: 4
|
|
7241
7585
|
}, reportData && /*#__PURE__*/React__namespace.default.createElement(ReportDataGrid, {
|
|
7242
7586
|
reportData: reportData,
|
|
7243
|
-
|
|
7244
|
-
metrics: report.metrics,
|
|
7245
|
-
computedDimensions: report.computedDimensions,
|
|
7587
|
+
columnItems: columnItems,
|
|
7246
7588
|
loading: loading,
|
|
7247
7589
|
onPageChange: handlePageChange,
|
|
7248
7590
|
totalRows: totalRows,
|
|
@@ -7305,6 +7647,50 @@ const ReportDefinitionsManager = () => {
|
|
|
7305
7647
|
});
|
|
7306
7648
|
};
|
|
7307
7649
|
|
|
7650
|
+
// TODO: replace with currencies fetched from the API once that's available.
|
|
7651
|
+
const CURRENCY_OPTIONS = [{
|
|
7652
|
+
key: "USD",
|
|
7653
|
+
value: "USD"
|
|
7654
|
+
}, {
|
|
7655
|
+
key: "EUR",
|
|
7656
|
+
value: "EUR"
|
|
7657
|
+
}, {
|
|
7658
|
+
key: "SEK",
|
|
7659
|
+
value: "SEK"
|
|
7660
|
+
}, {
|
|
7661
|
+
key: "GBP",
|
|
7662
|
+
value: "GBP"
|
|
7663
|
+
}, {
|
|
7664
|
+
key: "DKK",
|
|
7665
|
+
value: "DKK"
|
|
7666
|
+
}, {
|
|
7667
|
+
key: "NOK",
|
|
7668
|
+
value: "NOK"
|
|
7669
|
+
}];
|
|
7670
|
+
const CurrencySelector = () => {
|
|
7671
|
+
const {
|
|
7672
|
+
parameters,
|
|
7673
|
+
setParameters
|
|
7674
|
+
} = useReportingContext();
|
|
7675
|
+
|
|
7676
|
+
// Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
|
|
7677
|
+
const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
|
|
7678
|
+
if (!showCurrencySelector) {
|
|
7679
|
+
return null;
|
|
7680
|
+
}
|
|
7681
|
+
return /*#__PURE__*/React__namespace.default.createElement(SingleSelect, {
|
|
7682
|
+
items: CURRENCY_OPTIONS,
|
|
7683
|
+
value: parameters.base_currency,
|
|
7684
|
+
label: "Currency",
|
|
7685
|
+
onChange: e => setParameters({
|
|
7686
|
+
...parameters,
|
|
7687
|
+
base_currency: e.target.value
|
|
7688
|
+
}),
|
|
7689
|
+
sx: {
|
|
7690
|
+
width: 110
|
|
7691
|
+
}
|
|
7692
|
+
});
|
|
7693
|
+
};
|
|
7308
7694
|
const ReportApp = ({
|
|
7309
7695
|
params,
|
|
7310
7696
|
api
|
|
@@ -7371,7 +7757,13 @@ const ReportApp = ({
|
|
|
7371
7757
|
}
|
|
7372
7758
|
}
|
|
7373
7759
|
})
|
|
7374
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
7760
|
+
}, /*#__PURE__*/React__namespace.default.createElement(Box__default.default, {
|
|
7761
|
+
sx: {
|
|
7762
|
+
display: "flex",
|
|
7763
|
+
justifyContent: "flex-end",
|
|
7764
|
+
p: 1
|
|
7765
|
+
}
|
|
7766
|
+
}, /*#__PURE__*/React__namespace.default.createElement(CurrencySelector, null)), /*#__PURE__*/React__namespace.default.createElement(ReportDefinitionsManager, null))));
|
|
7375
7767
|
};
|
|
7376
7768
|
|
|
7377
7769
|
var index = {
|